Compare commits

..
Author SHA1 Message Date
Teknium e2d80b6885 feat(security): deny writes to startup and global git config
Block write tool edits to secondary shell startup files and global git config paths while preserving project-local `.git/config` writes.
2026-06-13 06:52:11 -07:00
7 changed files with 23 additions and 76 deletions
+5
View File
@@ -51,6 +51,10 @@ def build_write_denied_paths(home: str) -> set[str]:
os.path.join(home, ".profile"),
os.path.join(home, ".bash_profile"),
os.path.join(home, ".zprofile"),
os.path.join(home, ".zshenv"),
os.path.join(home, ".zlogin"),
os.path.join(home, ".bash_login"),
os.path.join(home, ".gitconfig"),
os.path.join(home, ".netrc"),
os.path.join(home, ".pgpass"),
os.path.join(home, ".npmrc"),
@@ -78,6 +82,7 @@ def build_write_denied_prefixes(home: str) -> list[str]:
os.path.join(home, ".azure"),
os.path.join(home, ".config", "gh"),
os.path.join(home, ".config", "gcloud"),
os.path.join(home, ".config", "git"),
]
]
+1 -26
View File
@@ -22,31 +22,9 @@ TitleCallback = Callable[[str], None]
_TITLE_PROMPT = (
"Generate a short, descriptive title (3-7 words) for a conversation that starts with the "
"following exchange. The title should capture the main topic or intent. "
"Write the title in the same language the user is writing in. "
"Return ONLY the title text, nothing else. No quotes, no punctuation at the end, no prefixes."
)
_TITLE_PROMPT_PINNED_LANGUAGE = (
"Generate a short, descriptive title (3-7 words) for a conversation that starts with the "
"following exchange. The title should capture the main topic or intent. "
"Write the title in {language}. "
"Return ONLY the title text, nothing else. No quotes, no punctuation at the end, no prefixes."
)
def _title_language() -> str:
"""Return configured title language, or empty string to match the user."""
try:
from hermes_cli.config import load_config
return str(
((load_config() or {}).get("auxiliary") or {})
.get("title_generation", {})
.get("language", "")
).strip()
except Exception:
return ""
def generate_title(
user_message: str,
@@ -70,11 +48,8 @@ def generate_title(
user_snippet = user_message[:500] if user_message else ""
assistant_snippet = assistant_response[:500] if assistant_response else ""
language = _title_language()
prompt = _TITLE_PROMPT_PINNED_LANGUAGE.format(language=language) if language else _TITLE_PROMPT
messages = [
{"role": "system", "content": prompt},
{"role": "system", "content": _TITLE_PROMPT},
{"role": "user", "content": f"User: {user_snippet}\n\nAssistant: {assistant_snippet}"},
]
-1
View File
@@ -1309,7 +1309,6 @@ DEFAULT_CONFIG = {
"api_key": "",
"timeout": 30,
"extra_body": {},
"language": "",
},
"tts_audio_tags": {
"provider": "auto",
-37
View File
@@ -7,7 +7,6 @@ from agent.title_generator import (
generate_title,
auto_title_session,
maybe_auto_title,
_title_language,
)
@@ -23,42 +22,6 @@ class TestGenerateTitle:
title = generate_title("help me fix this import", "Sure, let me check...")
assert title == "Debugging Python Import Errors"
def test_default_prompt_matches_user_language(self):
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Some Title"
with patch("agent.title_generator.call_llm", return_value=mock_response) as llm:
generate_title("質問です", "回答です")
system_prompt = llm.call_args.kwargs["messages"][0]["content"]
assert "same language the user is writing in" in system_prompt
def test_configured_language_pins_prompt(self):
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Some Title"
with (
patch("agent.title_generator.call_llm", return_value=mock_response) as llm,
patch("agent.title_generator._title_language", return_value="Japanese"),
):
generate_title("hello", "hi")
system_prompt = llm.call_args.kwargs["messages"][0]["content"]
assert "Write the title in Japanese" in system_prompt
assert "same language the user" not in system_prompt
def test_title_language_reads_config(self):
cfg = {"auxiliary": {"title_generation": {"language": " French "}}}
with patch("hermes_cli.config.load_config", return_value=cfg):
assert _title_language() == "French"
with patch("hermes_cli.config.load_config", return_value={}):
assert _title_language() == ""
with patch("hermes_cli.config.load_config", side_effect=RuntimeError("bad config")):
assert _title_language() == ""
def test_strips_quotes(self):
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
+16 -1
View File
@@ -69,9 +69,24 @@ class TestWriteDenyExactPaths:
def test_shell_profiles(self):
home = str(Path.home())
for name in [".bashrc", ".zshrc", ".profile", ".bash_profile", ".zprofile"]:
for name in [
".bashrc", ".zshrc", ".profile", ".bash_profile", ".zprofile",
".zshenv", ".zlogin", ".bash_login",
]:
assert _is_write_denied(os.path.join(home, name)) is True, f"{name} should be denied"
def test_global_git_config_paths(self):
home = str(Path.home())
for path in [
os.path.join(home, ".gitconfig"),
os.path.join(home, ".config", "git", "config"),
os.path.join(home, ".config", "git", "hooks", "pre-commit"),
]:
assert _is_write_denied(path) is True, f"{path} should be denied"
def test_project_git_config_allowed(self):
assert _is_write_denied("/tmp/someproject/.git/config") is False
def test_package_manager_configs(self):
home = str(Path.home())
for name in [".npmrc", ".pypirc", ".pgpass"]:
+1 -1
View File
@@ -252,7 +252,7 @@ THREAT_PATTERNS = [
(r'\bcrontab\b',
"persistence_cron", "medium", "persistence",
"modifies cron jobs"),
(r'\.(bashrc|zshrc|profile|bash_profile|bash_login|zprofile|zlogin)\b',
(r'\.(bashrc|zshrc|zshenv|profile|bash_profile|bash_login|zprofile|zlogin)\b',
"shell_rc_mod", "medium", "persistence",
"references shell startup file"),
(r'authorized_keys',
-10
View File
@@ -938,16 +938,6 @@ auxiliary:
compression:
timeout: 120 # seconds — compression summarizes long conversations, needs more time
# Auto-generated session titles. Empty language follows the conversation;
# set e.g. "English" or "Japanese" to pin titles to one language.
title_generation:
provider: "auto"
model: ""
base_url: ""
api_key: ""
timeout: 30
language: ""
# Skills hub — skill matching and search
skills_hub:
provider: "auto"