mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(security): apply file safety to copilot acp fs
This commit is contained in:
parent
517f5e2639
commit
9b36636363
5 changed files with 295 additions and 84 deletions
|
|
@ -35,6 +35,13 @@ from pathlib import Path
|
|||
from hermes_constants import get_hermes_home
|
||||
from tools.binary_extensions import BINARY_EXTENSIONS
|
||||
|
||||
from agent.file_safety import (
|
||||
build_write_denied_paths,
|
||||
build_write_denied_prefixes,
|
||||
get_safe_write_root as _shared_get_safe_write_root,
|
||||
is_write_denied as _shared_is_write_denied,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Write-path deny list — blocks writes to sensitive system/credential files
|
||||
|
|
@ -42,41 +49,9 @@ from tools.binary_extensions import BINARY_EXTENSIONS
|
|||
|
||||
_HOME = str(Path.home())
|
||||
|
||||
WRITE_DENIED_PATHS = {
|
||||
os.path.realpath(p) for p in [
|
||||
os.path.join(_HOME, ".ssh", "authorized_keys"),
|
||||
os.path.join(_HOME, ".ssh", "id_rsa"),
|
||||
os.path.join(_HOME, ".ssh", "id_ed25519"),
|
||||
os.path.join(_HOME, ".ssh", "config"),
|
||||
str(get_hermes_home() / ".env"),
|
||||
os.path.join(_HOME, ".bashrc"),
|
||||
os.path.join(_HOME, ".zshrc"),
|
||||
os.path.join(_HOME, ".profile"),
|
||||
os.path.join(_HOME, ".bash_profile"),
|
||||
os.path.join(_HOME, ".zprofile"),
|
||||
os.path.join(_HOME, ".netrc"),
|
||||
os.path.join(_HOME, ".pgpass"),
|
||||
os.path.join(_HOME, ".npmrc"),
|
||||
os.path.join(_HOME, ".pypirc"),
|
||||
"/etc/sudoers",
|
||||
"/etc/passwd",
|
||||
"/etc/shadow",
|
||||
]
|
||||
}
|
||||
WRITE_DENIED_PATHS = build_write_denied_paths(_HOME)
|
||||
|
||||
WRITE_DENIED_PREFIXES = [
|
||||
os.path.realpath(p) + os.sep for p in [
|
||||
os.path.join(_HOME, ".ssh"),
|
||||
os.path.join(_HOME, ".aws"),
|
||||
os.path.join(_HOME, ".gnupg"),
|
||||
os.path.join(_HOME, ".kube"),
|
||||
"/etc/sudoers.d",
|
||||
"/etc/systemd",
|
||||
os.path.join(_HOME, ".docker"),
|
||||
os.path.join(_HOME, ".azure"),
|
||||
os.path.join(_HOME, ".config", "gh"),
|
||||
]
|
||||
]
|
||||
WRITE_DENIED_PREFIXES = build_write_denied_prefixes(_HOME)
|
||||
|
||||
|
||||
def _get_safe_write_root() -> Optional[str]:
|
||||
|
|
@ -87,33 +62,12 @@ def _get_safe_write_root() -> Optional[str]:
|
|||
not on the static deny list. Opt-in hardening for gateway/messaging
|
||||
deployments that should only touch a workspace checkout.
|
||||
"""
|
||||
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
|
||||
return _shared_get_safe_write_root()
|
||||
|
||||
|
||||
def _is_write_denied(path: str) -> bool:
|
||||
"""Return True if path is on the write deny list."""
|
||||
resolved = os.path.realpath(os.path.expanduser(str(path)))
|
||||
|
||||
# 1) Static deny list
|
||||
if resolved in WRITE_DENIED_PATHS:
|
||||
return True
|
||||
for prefix in WRITE_DENIED_PREFIXES:
|
||||
if resolved.startswith(prefix):
|
||||
return True
|
||||
|
||||
# 2) Optional safe-root sandbox
|
||||
safe_root = _get_safe_write_root()
|
||||
if safe_root:
|
||||
if not (resolved == safe_root or resolved.startswith(safe_root + os.sep)):
|
||||
return True
|
||||
|
||||
return False
|
||||
return _shared_is_write_denied(path)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import logging
|
|||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from agent.file_safety import get_read_block_error
|
||||
from tools.binary_extensions import has_binary_extension
|
||||
from tools.file_operations import ShellFileOperations
|
||||
from agent.redact import redact_sensitive_text
|
||||
|
|
@ -373,24 +376,9 @@ def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str =
|
|||
|
||||
# ── Hermes internal path guard ────────────────────────────────
|
||||
# Prevent prompt injection via catalog or hub metadata files.
|
||||
from hermes_constants import get_hermes_home as _get_hh
|
||||
_hermes_home = _get_hh().resolve()
|
||||
_blocked_dirs = [
|
||||
_hermes_home / "skills" / ".hub" / "index-cache",
|
||||
_hermes_home / "skills" / ".hub",
|
||||
]
|
||||
for _blocked in _blocked_dirs:
|
||||
try:
|
||||
_resolved.relative_to(_blocked)
|
||||
return json.dumps({
|
||||
"error": (
|
||||
f"Access denied: {path} is an internal Hermes cache file "
|
||||
"and cannot be read directly to prevent prompt injection. "
|
||||
"Use the skills_list or skill_view tools instead."
|
||||
)
|
||||
})
|
||||
except ValueError:
|
||||
pass
|
||||
block_error = get_read_block_error(path)
|
||||
if block_error:
|
||||
return json.dumps({"error": block_error})
|
||||
|
||||
# ── Dedup check ───────────────────────────────────────────────
|
||||
# If we already read this exact (path, offset, limit) and the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue