fix(security): apply file safety to copilot acp fs

This commit is contained in:
ifrederico 2026-03-20 13:00:37 -04:00 committed by Teknium
parent 517f5e2639
commit 9b36636363
5 changed files with 295 additions and 84 deletions

View file

@ -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)
# =============================================================================

View file

@ -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