mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(file-safety): distinguish safe-root write denial from credential blocks
Return actionable errors when HERMES_WRITE_SAFE_ROOT blocks a path instead of labeling every denial as a protected credential file. Wire the helper through write_file, patch, delete/move, and the Copilot ACP shim; sync docs examples.
This commit is contained in:
parent
1b5ceec2a0
commit
55d826ccef
9 changed files with 87 additions and 27 deletions
|
|
@ -26,7 +26,7 @@ from openai.types.chat.chat_completion_message_tool_call import (
|
|||
Function,
|
||||
)
|
||||
|
||||
from agent.file_safety import get_read_block_error, is_write_denied
|
||||
from agent.file_safety import get_read_block_error, get_write_denied_error
|
||||
from agent.redact import redact_sensitive_text
|
||||
from tools.environments.local import hermes_subprocess_env
|
||||
|
||||
|
|
@ -727,10 +727,9 @@ class CopilotACPClient:
|
|||
elif method == "fs/write_text_file":
|
||||
try:
|
||||
path = _ensure_path_within_cwd(str(params.get("path") or ""), cwd)
|
||||
if is_write_denied(str(path)):
|
||||
raise PermissionError(
|
||||
f"Write denied: '{path}' is a protected system/credential file."
|
||||
)
|
||||
denied = get_write_denied_error(str(path))
|
||||
if denied:
|
||||
raise PermissionError(denied)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(str(params.get("content") or ""))
|
||||
response = {
|
||||
|
|
|
|||
|
|
@ -95,16 +95,16 @@ def get_safe_write_roots() -> set[str]:
|
|||
return roots
|
||||
|
||||
|
||||
def is_write_denied(path: str) -> bool:
|
||||
"""Return True if path is blocked by the write denylist or safe root."""
|
||||
def _classify_write_denial(path: str) -> Optional[str]:
|
||||
"""Return ``'credential'``, ``'safe_root'``, or ``None`` if writes are allowed."""
|
||||
home = os.path.realpath(os.path.expanduser("~"))
|
||||
resolved = os.path.realpath(os.path.expanduser(str(path)))
|
||||
|
||||
if resolved in build_write_denied_paths(home):
|
||||
return True
|
||||
return "credential"
|
||||
for prefix in build_write_denied_prefixes(home):
|
||||
if resolved.startswith(prefix):
|
||||
return True
|
||||
return "credential"
|
||||
|
||||
mcp_tokens_dir_name = "mcp-tokens"
|
||||
|
||||
|
|
@ -121,13 +121,13 @@ def is_write_denied(path: str) -> bool:
|
|||
try:
|
||||
mcp_real = os.path.realpath(os.path.join(base_real, mcp_tokens_dir_name))
|
||||
if resolved == mcp_real or resolved.startswith(mcp_real + os.sep):
|
||||
return True
|
||||
return "credential"
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
pairing_real = os.path.realpath(os.path.join(base_real, "pairing"))
|
||||
if resolved == pairing_real or resolved.startswith(pairing_real + os.sep):
|
||||
return True
|
||||
return "credential"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -139,9 +139,28 @@ def is_write_denied(path: str) -> bool:
|
|||
allowed = True
|
||||
break
|
||||
if not allowed:
|
||||
return True
|
||||
return "safe_root"
|
||||
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def is_write_denied(path: str) -> bool:
|
||||
"""Return True if path is blocked by the write denylist or safe root."""
|
||||
return _classify_write_denial(path) is not None
|
||||
|
||||
|
||||
def get_write_denied_error(path: str, *, verb: str = "Write") -> Optional[str]:
|
||||
"""Return a user/model-facing error when writes to ``path`` are blocked."""
|
||||
denial = _classify_write_denial(path)
|
||||
if denial is None:
|
||||
return None
|
||||
if denial == "safe_root":
|
||||
roots_display = os.pathsep.join(sorted(get_safe_write_roots()))
|
||||
return (
|
||||
f"{verb} denied: '{path}' is outside HERMES_WRITE_SAFE_ROOT "
|
||||
f"({roots_display}). Unset the variable or add this path's directory prefix."
|
||||
)
|
||||
return f"{verb} denied: '{path}' is a protected system/credential file."
|
||||
|
||||
|
||||
# Common secret-bearing project-local environment file basenames.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue