mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-27 11:22:03 +00:00
fix(config): quote env values containing hash
This commit is contained in:
parent
da73223f4a
commit
150afea942
2 changed files with 93 additions and 3 deletions
|
|
@ -6095,6 +6095,29 @@ def save_config(config: Dict[str, Any]):
|
|||
_LAST_EXPANDED_CONFIG_BY_PATH[str(config_path)] = copy.deepcopy(current_normalized)
|
||||
|
||||
|
||||
def _parse_env_value(raw_value: str) -> str:
|
||||
"""Parse the small .env value subset Hermes writes itself."""
|
||||
value = raw_value.strip()
|
||||
if len(value) >= 2 and value[0] == value[-1] == '"':
|
||||
quoted = value[1:-1]
|
||||
parsed: list[str] = []
|
||||
i = 0
|
||||
while i < len(quoted):
|
||||
ch = quoted[i]
|
||||
if ch == "\\" and i + 1 < len(quoted):
|
||||
next_ch = quoted[i + 1]
|
||||
if next_ch in {'"', "\\"}:
|
||||
parsed.append(next_ch)
|
||||
i += 2
|
||||
continue
|
||||
parsed.append(ch)
|
||||
i += 1
|
||||
return "".join(parsed)
|
||||
if len(value) >= 2 and value[0] == value[-1] == "'":
|
||||
return value[1:-1]
|
||||
return value
|
||||
|
||||
|
||||
def load_env() -> Dict[str, str]:
|
||||
"""Load environment variables from ~/.hermes/.env.
|
||||
|
||||
|
|
@ -6144,7 +6167,7 @@ def load_env() -> Dict[str, str]:
|
|||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, _, value = line.partition('=')
|
||||
env_vars[key.strip()] = value.strip().strip('"\'')
|
||||
env_vars[key.strip()] = _parse_env_value(value)
|
||||
|
||||
if cache_key is not None:
|
||||
_env_cache = (cache_key, dict(env_vars))
|
||||
|
|
@ -6318,6 +6341,22 @@ def _check_non_ascii_credential(key: str, value: str) -> str:
|
|||
return sanitized
|
||||
|
||||
|
||||
def _quote_env_value(value: str) -> str:
|
||||
"""Quote .env values containing characters with special dotenv meaning."""
|
||||
if value == "":
|
||||
return value
|
||||
needs_quoting = (
|
||||
"#" in value
|
||||
or '"' in value
|
||||
or "'" in value
|
||||
or value != value.strip()
|
||||
)
|
||||
if not needs_quoting:
|
||||
return value
|
||||
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def save_env_value(key: str, value: str):
|
||||
"""Save or update a value in ~/.hermes/.env."""
|
||||
if is_managed():
|
||||
|
|
@ -6357,11 +6396,13 @@ def save_env_value(key: str, value: str):
|
|||
# Sanitize on every read: split concatenated keys, drop stale placeholders
|
||||
lines = _sanitize_env_lines(lines)
|
||||
|
||||
serialized_value = _quote_env_value(value)
|
||||
|
||||
# Find and update or append
|
||||
found = False
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip().startswith(f"{key}="):
|
||||
lines[i] = f"{key}={value}\n"
|
||||
lines[i] = f"{key}={serialized_value}\n"
|
||||
found = True
|
||||
break
|
||||
|
||||
|
|
@ -6369,7 +6410,7 @@ def save_env_value(key: str, value: str):
|
|||
# Ensure there's a newline at the end of the file before appending
|
||||
if lines and not lines[-1].endswith("\n"):
|
||||
lines[-1] += "\n"
|
||||
lines.append(f"{key}={value}\n")
|
||||
lines.append(f"{key}={serialized_value}\n")
|
||||
|
||||
fd, tmp_path = tempfile.mkstemp(dir=str(env_path.parent), suffix='.tmp', prefix='.env_')
|
||||
# Preserve original permissions so Docker volume mounts aren't clobbered.
|
||||
|
|
|
|||
|
|
@ -354,6 +354,55 @@ class TestSaveEnvValueSecure:
|
|||
env_mode = env_path.stat().st_mode & 0o777
|
||||
assert env_mode == 0o640, f"expected 0o640, got {oct(env_mode)}"
|
||||
|
||||
def test_save_env_value_quotes_values_containing_hash(self, tmp_path):
|
||||
"""Regression test for #30355."""
|
||||
from dotenv import dotenv_values
|
||||
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}, clear=False):
|
||||
os.environ.pop("ANTHROPIC_TOKEN", None)
|
||||
token = "sk-ant-oat01-abc#xyz#more"
|
||||
save_env_value("ANTHROPIC_TOKEN", token)
|
||||
|
||||
content = (tmp_path / ".env").read_text(encoding="utf-8")
|
||||
assert f'ANTHROPIC_TOKEN="{token}"' in content
|
||||
|
||||
parsed = dotenv_values(str(tmp_path / ".env"))
|
||||
assert parsed["ANTHROPIC_TOKEN"] == token
|
||||
assert load_env()["ANTHROPIC_TOKEN"] == token
|
||||
|
||||
def test_save_env_value_hash_value_round_trips_quotes_and_backslashes(self, tmp_path):
|
||||
from dotenv import dotenv_values
|
||||
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}, clear=False):
|
||||
os.environ.pop("ANTHROPIC_TOKEN", None)
|
||||
token = 'abc"def\\ghi#jkl'
|
||||
save_env_value("ANTHROPIC_TOKEN", token)
|
||||
|
||||
content = (tmp_path / ".env").read_text(encoding="utf-8")
|
||||
assert 'ANTHROPIC_TOKEN="abc\\"def\\\\ghi#jkl"' in content
|
||||
|
||||
parsed = dotenv_values(str(tmp_path / ".env"))
|
||||
assert parsed["ANTHROPIC_TOKEN"] == token
|
||||
assert load_env()["ANTHROPIC_TOKEN"] == token
|
||||
|
||||
def test_save_env_value_updates_hash_value_with_quotes(self, tmp_path):
|
||||
from dotenv import dotenv_values
|
||||
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}, clear=False):
|
||||
os.environ.pop("ANTHROPIC_TOKEN", None)
|
||||
save_env_value("ANTHROPIC_TOKEN", "old-token")
|
||||
|
||||
token = 'abc"def\\ghi#jkl'
|
||||
save_env_value("ANTHROPIC_TOKEN", token)
|
||||
|
||||
content = (tmp_path / ".env").read_text(encoding="utf-8")
|
||||
assert content.count("ANTHROPIC_TOKEN=") == 1
|
||||
assert 'ANTHROPIC_TOKEN="abc\\"def\\\\ghi#jkl"' in content
|
||||
|
||||
parsed = dotenv_values(str(tmp_path / ".env"))
|
||||
assert parsed["ANTHROPIC_TOKEN"] == token
|
||||
assert load_env()["ANTHROPIC_TOKEN"] == token
|
||||
|
||||
|
||||
class TestRemoveEnvValue:
|
||||
def test_removes_key_from_env_file(self, tmp_path):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue