Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions agent/file_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,21 @@ def build_write_denied_prefixes(home: str) -> list[str]:
]


def get_safe_write_root() -> Optional[str]:
"""Return the resolved HERMES_WRITE_SAFE_ROOT path, or None if unset."""
root = os.getenv("HERMES_WRITE_SAFE_ROOT", "")
if not root:
return None
try:
return os.path.realpath(os.path.expanduser(root))
except Exception:
return None
def get_safe_write_roots() -> set[str]:
"""Return resolved HERMES_WRITE_SAFE_ROOT paths. Supports multiple directories
separated by ':' (Unix PATH-style). E.g., '/opt/data:/var/www/html'."""
env = os.getenv("HERMES_WRITE_SAFE_ROOT", "")
if not env:
return set()
roots: set[str] = set()
for path in env.split(":"):
if path:
try:
resolved = os.path.realpath(os.path.expanduser(path))
roots.add(resolved)
except Exception:
continue
return roots


def is_write_denied(path: str) -> bool:
Expand Down Expand Up @@ -124,9 +130,16 @@ def is_write_denied(path: str) -> bool:
except Exception:
pass

safe_root = get_safe_write_root()
if safe_root and not (resolved == safe_root or resolved.startswith(safe_root + os.sep)):
return True
safe_roots = get_safe_write_roots()
if safe_roots:
# Allow write if path is under ANY of the safe roots
allowed = False
for safe_root in safe_roots:
if resolved == safe_root or resolved.startswith(safe_root + os.sep):
allowed = True
break
if not allowed:
return True

return False

Expand Down
4 changes: 2 additions & 2 deletions tools/memory_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ def _read_file(path: Path) -> List[str]:
return []
try:
raw = path.read_text(encoding="utf-8")
except (OSError, IOError):
except (OSError, IOError, UnicodeDecodeError):
return []

if not raw.strip():
Expand Down Expand Up @@ -673,7 +673,7 @@ def _detect_external_drift(self, target: str) -> Optional[str]:
return None
try:
raw = path.read_text(encoding="utf-8")
except (OSError, IOError):
except (OSError, IOError, UnicodeDecodeError):
return None
if not raw.strip():
return None
Expand Down
2 changes: 1 addition & 1 deletion website/docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us
| `HERMES_PREFILL_MESSAGES_FILE` | Path to a JSON file of ephemeral prefill messages injected at API-call time. |
| `HERMES_ALLOW_PRIVATE_URLS` | `true`/`false` — allow tools to fetch localhost/private-network URLs. Off by default in gateway mode. |
| `HERMES_REDACT_SECRETS` | `true`/`false` — control secret redaction in tool output, logs, and chat responses (default: `true`). |
| `HERMES_WRITE_SAFE_ROOT` | Optional directory prefix that restricts `write_file`/`patch` writes; paths outside require approval. |
| `HERMES_WRITE_SAFE_ROOT` | Optional directory prefix that restricts `write_file`/`patch` writes; paths outside require approval. Supports multiple directories separated by `:` (e.g., `/opt/data:/var/www/html`). |
| `HERMES_DISABLE_LAZY_INSTALLS` | Internal bridge var set automatically in the official Docker image to prevent runtime dependency installs into the immutable `/opt/hermes` tree. The user-facing equivalent is `security.allow_lazy_installs: false` in `config.yaml`; do not set this in `.env`. |
| `HERMES_DISABLE_FILE_STATE_GUARD` | Set to `1` to turn off the "file changed since you read it" guard on `patch`/`write_file`. |
| `HERMES_CORE_TOOLS` | Comma-separated override for the canonical core tool list (advanced; rarely needed). |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ Graph 事件(Teams 会议、日历、聊天等)的入站变更通知监听
| `HERMES_PREFILL_MESSAGES_FILE` | 包含在 API 调用时注入的临时预填消息的 JSON 文件路径。 |
| `HERMES_ALLOW_PRIVATE_URLS` | `true`/`false`——允许工具获取 localhost/私有网络 URL。gateway 模式下默认关闭。 |
| `HERMES_REDACT_SECRETS` | `true`/`false`——控制工具输出、日志和聊天响应中的密钥脱敏(默认:`true`)。 |
| `HERMES_WRITE_SAFE_ROOT` | 可选目录前缀,限制 `write_file`/`patch` 写入;超出范围的路径需要审批。 |
| `HERMES_WRITE_SAFE_ROOT` | 可选目录前缀,限制 `write_file`/`patch` 写入;超出范围的路径需要审批。支持多个目录,使用 `:` 分隔(例如:`/opt/data:/var/www/html`)。 |
| `HERMES_DISABLE_FILE_STATE_GUARD` | 设为 `1` 可关闭 `patch`/`write_file` 上的"文件自上次读取后已更改"保护。 |
| `HERMES_CORE_TOOLS` | 规范核心工具列表的逗号分隔覆盖(高级;极少需要)。 |
| `HERMES_BUNDLED_SKILLS` | 启动时加载的内置技能列表的逗号分隔覆盖。 |
Expand Down
Loading