atbash-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,31 @@
1
+ Atbash CLI -- Proprietary Software License
2
+
3
+ Copyright (c) 2026 Atbash. All rights reserved.
4
+
5
+ The full license is available at https://atbash.ai/license
6
+
7
+ Redistribution and use in source and binary forms, with or without modification,
8
+ are permitted provided that the following conditions are met:
9
+
10
+ Redistributions of source code must retain the above copyright notice, this list
11
+ of conditions and the following disclaimer.
12
+
13
+ Redistributions in binary form must reproduce the above copyright notice, this
14
+ list of conditions and the following disclaimer in the documentation and/or other
15
+ materials provided with the distribution.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
22
+ OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
26
+ IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
27
+ OF SUCH DAMAGE.
28
+
29
+ The views and conclusions contained in the software and documentation are those
30
+ of the authors and should not be interpreted as representing official policies,
31
+ either expressed or implied, of Atbash.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: atbash-hermes-plugin
3
+ Version: 0.1.0
4
+ Summary: Atbash safety plugin for Hermes Agent
5
+ Author: atbash
6
+ License-Expression: LicenseRef-Atbash-Proprietary
7
+ Project-URL: Homepage, https://github.com/Atbash-Ai/atbash-hermes-plugin
8
+ Project-URL: Repository, https://github.com/Atbash-Ai/atbash-hermes-plugin
9
+ Keywords: atbash,hermes,hermes-agent,agent-safety,ai-safety,tool-guard,judge,policy
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: atbash-sdk==0.1.3
14
+ Dynamic: license-file
15
+
16
+ # atbash-hermes-plugin
17
+
18
+ Atbash guardrail plugin for Hermes Agent using the official Python SDK (`atbash-sdk`).
19
+
20
+ ## What it does
21
+
22
+ - Intercepts every Hermes tool call in `pre_tool_call`
23
+ - Sends a verdict request to Atbash before the tool executes
24
+ - Blocks tool execution when verdict is denied
25
+
26
+ ## Install
27
+
28
+ 1. Clone/copy this folder into your Hermes plugins directory.
29
+ 2. Install dependencies:
30
+
31
+ ```bash
32
+ python3 -m pip install -r requirements.txt
33
+ ```
34
+
35
+ 3. Configure env vars:
36
+
37
+ ```bash
38
+ export ATBASH_AGENT_PRIVKEY='{"pubkey":"...","privkey":"..."}'
39
+ # or
40
+ export ATBASH_KEY_PATH="$HOME/.config/atbash/guard-client-key"
41
+
42
+ # optional
43
+ export ATBASH_ENDPOINT="https://api.atbash.io"
44
+ export ATBASH_ENFORCE_DECISION="true"
45
+ export ATBASH_DEBUG="false"
46
+ export ATBASH_TOOL_MAP_PATH="$HOME/.config/atbash/hermes-tool-map.json"
47
+ ```
48
+
49
+ 4. Enable plugin in Hermes config.
50
+
51
+ ## Tool classification
52
+
53
+ The plugin records previously unseen Hermes tool names in
54
+ `~/.config/atbash/hermes-tool-map.json`. The learned mappings are loaded when
55
+ each Hermes session starts, so tool classifications persist across restarts.
56
+ Set `ATBASH_TOOL_MAP_PATH` to override the location.
57
+
58
+ ## Behavior
59
+
60
+ - `ALLOW` => tool proceeds
61
+ - `HOLD` => tool is paused and must be reviewed before retrying
62
+ - `BLOCK`/`DENY`/`REJECT` => tool is blocked with a user-facing message
63
+ - Errors talking to Atbash:
64
+ - if `ATBASH_ENFORCE_DECISION=true` (default): block fail-closed
65
+ - if `false`: allow fail-open
@@ -0,0 +1,50 @@
1
+ # atbash-hermes-plugin
2
+
3
+ Atbash guardrail plugin for Hermes Agent using the official Python SDK (`atbash-sdk`).
4
+
5
+ ## What it does
6
+
7
+ - Intercepts every Hermes tool call in `pre_tool_call`
8
+ - Sends a verdict request to Atbash before the tool executes
9
+ - Blocks tool execution when verdict is denied
10
+
11
+ ## Install
12
+
13
+ 1. Clone/copy this folder into your Hermes plugins directory.
14
+ 2. Install dependencies:
15
+
16
+ ```bash
17
+ python3 -m pip install -r requirements.txt
18
+ ```
19
+
20
+ 3. Configure env vars:
21
+
22
+ ```bash
23
+ export ATBASH_AGENT_PRIVKEY='{"pubkey":"...","privkey":"..."}'
24
+ # or
25
+ export ATBASH_KEY_PATH="$HOME/.config/atbash/guard-client-key"
26
+
27
+ # optional
28
+ export ATBASH_ENDPOINT="https://api.atbash.io"
29
+ export ATBASH_ENFORCE_DECISION="true"
30
+ export ATBASH_DEBUG="false"
31
+ export ATBASH_TOOL_MAP_PATH="$HOME/.config/atbash/hermes-tool-map.json"
32
+ ```
33
+
34
+ 4. Enable plugin in Hermes config.
35
+
36
+ ## Tool classification
37
+
38
+ The plugin records previously unseen Hermes tool names in
39
+ `~/.config/atbash/hermes-tool-map.json`. The learned mappings are loaded when
40
+ each Hermes session starts, so tool classifications persist across restarts.
41
+ Set `ATBASH_TOOL_MAP_PATH` to override the location.
42
+
43
+ ## Behavior
44
+
45
+ - `ALLOW` => tool proceeds
46
+ - `HOLD` => tool is paused and must be reviewed before retrying
47
+ - `BLOCK`/`DENY`/`REJECT` => tool is blocked with a user-facing message
48
+ - Errors talking to Atbash:
49
+ - if `ATBASH_ENFORCE_DECISION=true` (default): block fail-closed
50
+ - if `false`: allow fail-open
@@ -0,0 +1,274 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Optional
8
+
9
+ logger = logging.getLogger("atbash-hermes-plugin")
10
+
11
+ DEFAULT_TOOL_CLASS_MAP = {
12
+ "browser_click": "browser_interaction",
13
+ "browser_navigate": "network",
14
+ "browser_type": "browser_interaction",
15
+ "read_file": "filesystem_read",
16
+ "skill_view": "skill_read",
17
+ "terminal": "command_execution",
18
+ "write_file": "filesystem_write",
19
+ }
20
+
21
+
22
+ def _default_tool_map_path() -> Path:
23
+ return Path("~/.config/atbash/hermes-tool-map.json").expanduser()
24
+
25
+
26
+ def _load_tool_map(path: Path) -> Dict[str, str]:
27
+ try:
28
+ raw = json.loads(path.read_text(encoding="utf-8"))
29
+ except FileNotFoundError:
30
+ return {}
31
+ except Exception as e:
32
+ logger.warning("Could not read Atbash Hermes tool map path=%s err=%s", path, e)
33
+ return {}
34
+
35
+ if not isinstance(raw, dict):
36
+ logger.warning("Ignoring invalid Atbash Hermes tool map path=%s", path)
37
+ return {}
38
+ return {
39
+ str(tool_name): str(action_class)
40
+ for tool_name, action_class in raw.items()
41
+ if isinstance(tool_name, str) and isinstance(action_class, str)
42
+ }
43
+
44
+
45
+ def _save_tool_map(path: Path, tool_map: Dict[str, str]) -> None:
46
+ try:
47
+ path.parent.mkdir(parents=True, exist_ok=True)
48
+ temp_path = path.with_suffix(f"{path.suffix}.tmp")
49
+ temp_path.write_text(
50
+ json.dumps(dict(sorted(tool_map.items())), indent=2) + "\n",
51
+ encoding="utf-8",
52
+ )
53
+ temp_path.replace(path)
54
+ except Exception as e:
55
+ logger.warning("Could not persist Atbash Hermes tool map path=%s err=%s", path, e)
56
+
57
+
58
+
59
+
60
+ def _extract_command(tool_name: str, tool_args: Dict[str, Any]) -> str:
61
+ if not isinstance(tool_args, dict):
62
+ return ""
63
+ for key in ("command", "cmd", "shell_command", "bash_command", "zsh_command", "script"):
64
+ v = tool_args.get(key)
65
+ if isinstance(v, str) and v.strip():
66
+ return v.strip()
67
+ # Hermes terminal/shell tools sometimes nest payloads
68
+ for key in ("input", "args", "payload"):
69
+ v = tool_args.get(key)
70
+ if isinstance(v, dict):
71
+ for k in ("command", "cmd"):
72
+ vv = v.get(k)
73
+ if isinstance(vv, str) and vv.strip():
74
+ return vv.strip()
75
+ return ""
76
+
77
+ def _infer_action_class(tool_name: str) -> str:
78
+ t = (tool_name or "").lower()
79
+ if any(k in t for k in ("write", "edit", "delete", "create", "mv", "cp")):
80
+ return "filesystem_write"
81
+ if any(k in t for k in ("read", "list", "ls", "glob", "find", "search")):
82
+ return "filesystem_read"
83
+ if any(k in t for k in ("shell", "terminal", "exec", "command", "bash", "zsh")):
84
+ return "command_execution"
85
+ if any(k in t for k in ("http", "web", "fetch", "request", "browser", "mcp")):
86
+ return "network"
87
+ if any(k in t for k in ("image", "video", "audio")):
88
+ return "media_generation"
89
+ return "unknown"
90
+
91
+
92
+ def _as_bool(value: Optional[str], default: bool) -> bool:
93
+ if value is None:
94
+ return default
95
+ return value.strip().lower() in {"1", "true", "yes", "on"}
96
+
97
+
98
+ def _normalize_verdict(raw: Any) -> str:
99
+ # atbash-sdk JudgeResult object path
100
+ verdict_attr = getattr(raw, "verdict", None)
101
+ if verdict_attr is None:
102
+ verdict_attr = getattr(raw, "decision", None)
103
+ if isinstance(verdict_attr, str) and verdict_attr.strip():
104
+ return verdict_attr.strip().upper()
105
+
106
+ if raw is None:
107
+ return "ERROR"
108
+ if isinstance(raw, str):
109
+ return raw.strip().upper()
110
+ if isinstance(raw, dict):
111
+ for key in ("verdict", "decision", "status", "result"):
112
+ v = raw.get(key)
113
+ if isinstance(v, str) and v.strip():
114
+ return v.strip().upper()
115
+ return str(raw).strip().upper()
116
+
117
+
118
+ def _extract_reason(raw: Any) -> str:
119
+ reason_attr = getattr(raw, "reason", None)
120
+ if reason_attr is None:
121
+ reason_attr = getattr(raw, "message", None)
122
+ if isinstance(reason_attr, str) and reason_attr.strip():
123
+ return reason_attr
124
+
125
+ if isinstance(raw, str):
126
+ return raw
127
+ if isinstance(raw, dict):
128
+ for key in ("reason", "message", "explanation", "details"):
129
+ v = raw.get(key)
130
+ if isinstance(v, str) and v.strip():
131
+ return v
132
+ try:
133
+ return json.dumps(raw, ensure_ascii=True)
134
+ except Exception:
135
+ return str(raw)
136
+ return str(raw)
137
+
138
+
139
+ class AtbashHermesGuard:
140
+ def __init__(self) -> None:
141
+ self.debug = _as_bool(os.getenv("ATBASH_DEBUG"), False)
142
+ self.fail_closed = _as_bool(os.getenv("ATBASH_ENFORCE_DECISION"), True)
143
+ self.endpoint = os.getenv("ATBASH_ENDPOINT")
144
+ self.key_path = os.getenv("ATBASH_KEY_PATH")
145
+ self.privkey = os.getenv("ATBASH_AGENT_PRIVKEY")
146
+ self.provider = os.getenv("HERMES_PROVIDER")
147
+ self.model = os.getenv("HERMES_MODEL")
148
+ self.tool_map_path = Path(
149
+ os.getenv("ATBASH_TOOL_MAP_PATH", str(_default_tool_map_path()))
150
+ ).expanduser()
151
+ self.tool_map = {**DEFAULT_TOOL_CLASS_MAP, **_load_tool_map(self.tool_map_path)}
152
+
153
+ self.client = self._build_client()
154
+
155
+ def _action_class(self, tool_name: str) -> str:
156
+ mapped = self.tool_map.get(tool_name)
157
+ if mapped:
158
+ return mapped
159
+
160
+ action_class = _infer_action_class(tool_name)
161
+ self.tool_map[tool_name or "unknown"] = action_class
162
+ _save_tool_map(self.tool_map_path, self.tool_map)
163
+ logger.info(
164
+ "Atbash learned tool mapping tool=%s action_class=%s path=%s",
165
+ tool_name or "unknown",
166
+ action_class,
167
+ self.tool_map_path,
168
+ )
169
+ return action_class
170
+
171
+ def _build_client(self):
172
+ try:
173
+ from atbash import Atbash, ToolCallInput # type: ignore
174
+ except Exception as e:
175
+ try:
176
+ from atbash import Atbash # type: ignore
177
+ from atbash.types import ToolCallInput # type: ignore
178
+ except Exception:
179
+ raise RuntimeError("atbash-sdk is required. Install with: pip install atbash-sdk==0.1.3") from e
180
+
181
+ self.ToolCallInput = ToolCallInput
182
+ judge_cfg = {"endpoint": self.endpoint} if self.endpoint else None
183
+
184
+ # Prefer explicit key material if provided.
185
+ if self.privkey:
186
+ try:
187
+ return Atbash(self.privkey, endpoint=self.endpoint or "https://atbash.ai")
188
+ except TypeError:
189
+ return Atbash(self.privkey)
190
+
191
+ # Use SDK-native config loader for key file/env/config.
192
+ return Atbash.from_config(
193
+ key_path=self.key_path,
194
+ judge=judge_cfg,
195
+ fail_closed=self.fail_closed,
196
+ )
197
+
198
+ def _judge(self, tool_name: str, tool_args: Dict[str, Any], session_id: str = "", task_id: str = "", tool_call_id: str = "") -> Any:
199
+ action_class = self._action_class(tool_name)
200
+ command = _extract_command(tool_name, tool_args or {})
201
+
202
+ # Mirror OpenClaw plugin semantics: pass toolName + full args.
203
+ # Keep context concise but informative; args already carry command/body.
204
+ context = json.dumps(
205
+ {
206
+ "tool_name": tool_name,
207
+ "action_class": action_class,
208
+ "session_id": session_id or None,
209
+ "task_id": task_id or None,
210
+ "tool_call_id": tool_call_id or None,
211
+ "command": command or None,
212
+ "cwd": os.getcwd(),
213
+ },
214
+ ensure_ascii=True,
215
+ )
216
+
217
+ # Make action payload explicit so Atbash UI shows tool name, not only raw args.
218
+ action_payload = {
219
+ "tool_name": tool_name or "unknown",
220
+ "args": tool_args or {},
221
+ }
222
+
223
+ return self.client.audit_tool_call(
224
+ self.ToolCallInput(
225
+ tool_name=tool_name or "unknown",
226
+ args=action_payload,
227
+ context=context,
228
+ )
229
+ )
230
+
231
+ def pre_tool_call(self, *, tool_name: str = "", args: Dict[str, Any] | None = None, task_id: str = "", session_id: str = "", tool_call_id: str = ""):
232
+ if self.debug:
233
+ logger.info("Atbash pre_tool_call enter tool=%s session_id=%s task_id=%s tool_call_id=%s", tool_name, session_id, task_id, tool_call_id)
234
+ try:
235
+ verdict_raw = self._judge(
236
+ tool_name=tool_name,
237
+ tool_args=args or {},
238
+ session_id=session_id,
239
+ task_id=task_id,
240
+ tool_call_id=tool_call_id,
241
+ )
242
+ verdict = _normalize_verdict(verdict_raw)
243
+ reason = _extract_reason(verdict_raw)
244
+
245
+ if self.debug:
246
+ logger.info("Atbash verdict tool=%s verdict=%s reason=%s", tool_name, verdict, reason)
247
+
248
+ if verdict == "HOLD":
249
+ return {
250
+ "action": "block",
251
+ "message": f"Held by Atbash for operator review: {reason}",
252
+ }
253
+
254
+ if verdict in {"BLOCK", "DENY", "REJECT", "DISALLOW"}:
255
+ return {
256
+ "action": "block",
257
+ "message": f"Blocked by Atbash policy: {reason}",
258
+ }
259
+
260
+ return None
261
+ except Exception as e:
262
+ logger.warning("Atbash guard error tool=%s err=%s", tool_name, e)
263
+ if self.fail_closed:
264
+ return {
265
+ "action": "block",
266
+ "message": f"Blocked (Atbash unavailable): {e}",
267
+ }
268
+ return None
269
+
270
+
271
+ def register(ctx):
272
+ guard = AtbashHermesGuard()
273
+ ctx.register_hook("pre_tool_call", guard.pre_tool_call)
274
+ logger.info("[atbash-hermes-plugin] registered pre_tool_call hook")
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: atbash-hermes-plugin
3
+ Version: 0.1.0
4
+ Summary: Atbash safety plugin for Hermes Agent
5
+ Author: atbash
6
+ License-Expression: LicenseRef-Atbash-Proprietary
7
+ Project-URL: Homepage, https://github.com/Atbash-Ai/atbash-hermes-plugin
8
+ Project-URL: Repository, https://github.com/Atbash-Ai/atbash-hermes-plugin
9
+ Keywords: atbash,hermes,hermes-agent,agent-safety,ai-safety,tool-guard,judge,policy
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: atbash-sdk==0.1.3
14
+ Dynamic: license-file
15
+
16
+ # atbash-hermes-plugin
17
+
18
+ Atbash guardrail plugin for Hermes Agent using the official Python SDK (`atbash-sdk`).
19
+
20
+ ## What it does
21
+
22
+ - Intercepts every Hermes tool call in `pre_tool_call`
23
+ - Sends a verdict request to Atbash before the tool executes
24
+ - Blocks tool execution when verdict is denied
25
+
26
+ ## Install
27
+
28
+ 1. Clone/copy this folder into your Hermes plugins directory.
29
+ 2. Install dependencies:
30
+
31
+ ```bash
32
+ python3 -m pip install -r requirements.txt
33
+ ```
34
+
35
+ 3. Configure env vars:
36
+
37
+ ```bash
38
+ export ATBASH_AGENT_PRIVKEY='{"pubkey":"...","privkey":"..."}'
39
+ # or
40
+ export ATBASH_KEY_PATH="$HOME/.config/atbash/guard-client-key"
41
+
42
+ # optional
43
+ export ATBASH_ENDPOINT="https://api.atbash.io"
44
+ export ATBASH_ENFORCE_DECISION="true"
45
+ export ATBASH_DEBUG="false"
46
+ export ATBASH_TOOL_MAP_PATH="$HOME/.config/atbash/hermes-tool-map.json"
47
+ ```
48
+
49
+ 4. Enable plugin in Hermes config.
50
+
51
+ ## Tool classification
52
+
53
+ The plugin records previously unseen Hermes tool names in
54
+ `~/.config/atbash/hermes-tool-map.json`. The learned mappings are loaded when
55
+ each Hermes session starts, so tool classifications persist across restarts.
56
+ Set `ATBASH_TOOL_MAP_PATH` to override the location.
57
+
58
+ ## Behavior
59
+
60
+ - `ALLOW` => tool proceeds
61
+ - `HOLD` => tool is paused and must be reviewed before retrying
62
+ - `BLOCK`/`DENY`/`REJECT` => tool is blocked with a user-facing message
63
+ - Errors talking to Atbash:
64
+ - if `ATBASH_ENFORCE_DECISION=true` (default): block fail-closed
65
+ - if `false`: allow fail-open
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ atbash_hermes_plugin/__init__.py
5
+ atbash_hermes_plugin.egg-info/PKG-INFO
6
+ atbash_hermes_plugin.egg-info/SOURCES.txt
7
+ atbash_hermes_plugin.egg-info/dependency_links.txt
8
+ atbash_hermes_plugin.egg-info/entry_points.txt
9
+ atbash_hermes_plugin.egg-info/requires.txt
10
+ atbash_hermes_plugin.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [hermes_agent.plugins]
2
+ atbash-hermes-plugin = atbash_hermes_plugin
@@ -0,0 +1 @@
1
+ atbash-sdk==0.1.3
@@ -0,0 +1 @@
1
+ atbash_hermes_plugin
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "atbash-hermes-plugin"
7
+ version = "0.1.0"
8
+ description = "Atbash safety plugin for Hermes Agent"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [
12
+ { name = "atbash" }
13
+ ]
14
+ license = "LicenseRef-Atbash-Proprietary"
15
+ license-files = ["LICENSE"]
16
+ keywords = [
17
+ "atbash",
18
+ "hermes",
19
+ "hermes-agent",
20
+ "agent-safety",
21
+ "ai-safety",
22
+ "tool-guard",
23
+ "judge",
24
+ "policy"
25
+ ]
26
+ dependencies = [
27
+ "atbash-sdk==0.1.3"
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/Atbash-Ai/atbash-hermes-plugin"
32
+ Repository = "https://github.com/Atbash-Ai/atbash-hermes-plugin"
33
+
34
+ [project.entry-points."hermes_agent.plugins"]
35
+ atbash-hermes-plugin = "atbash_hermes_plugin"
36
+
37
+ [tool.setuptools.packages.find]
38
+ include = ["atbash_hermes_plugin*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+