Compare commits

..

2 Commits

Author SHA1 Message Date
alt-glitch
9aed1b2fe7 fix: restore accidentally deleted websocket close code assertion 2026-04-25 10:06:46 +05:30
alt-glitch
f87dbdf0a8 fix(web): reject empty values in PUT /api/env
The endpoint accepted empty strings, allowing any .env key to be
silently blanked out from the web UI. Add Pydantic validators to
reject empty keys and values.
2026-04-25 09:59:11 +05:30
4 changed files with 47 additions and 20 deletions

View File

@@ -867,8 +867,7 @@ def setup_model_provider(config: dict, *, quick: bool = False):
)
else:
_selected_vision_model = prompt(" Vision model (blank = use main/custom default)").strip()
if _selected_vision_model:
save_env_value("AUXILIARY_VISION_MODEL", _selected_vision_model)
save_env_value("AUXILIARY_VISION_MODEL", _selected_vision_model)
print_success(
f"Vision configured with {_base_url}"
+ (f" ({_selected_vision_model})" if _selected_vision_model else "")

View File

@@ -53,7 +53,7 @@ try:
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from pydantic import BaseModel, field_validator
except ImportError:
raise SystemExit(
"Web UI requires fastapi and uvicorn.\n"
@@ -425,6 +425,20 @@ class EnvVarUpdate(BaseModel):
key: str
value: str
@field_validator("key")
@classmethod
def key_must_be_nonempty(cls, v: str) -> str:
if not v.strip():
raise ValueError("key must not be empty")
return v
@field_validator("value")
@classmethod
def value_must_be_nonempty(cls, v: str) -> str:
if not v.strip():
raise ValueError("value must not be empty; use DELETE /api/env to remove a key")
return v
class EnvVarDelete(BaseModel):
key: str

View File

@@ -527,20 +527,3 @@ def test_offer_launch_chat_manual_fallback_when_unresolvable(monkeypatch, capsys
captured = capsys.readouterr()
assert "Run 'hermes chat' manually" in captured.out
class TestVisionModelBlankGuard:
"""Regression: blank vision model input must not overwrite existing .env value."""
def test_save_env_value_overwrites_with_empty(self, tmp_path, monkeypatch):
"""Proves save_env_value has no internal guard — caller must guard."""
env_path = tmp_path / ".env"
env_path.write_text("AUXILIARY_VISION_MODEL=custom-model\n")
monkeypatch.setattr("hermes_cli.config.get_env_path", lambda: env_path)
from hermes_cli.config import save_env_value
save_env_value("AUXILIARY_VISION_MODEL", "")
# save_env_value DOES overwrite — this is by design.
# The guard must be at the call site in setup.py.
assert "AUXILIARY_VISION_MODEL=\n" in env_path.read_text()

View File

@@ -1925,3 +1925,34 @@ class TestPtyWebSocket:
):
pass
assert exc.value.code == 4400
class TestEnvVarUpdateValidation:
"""PUT /api/env must reject empty values to prevent .env key destruction."""
def test_rejects_empty_value(self):
from hermes_cli.web_server import EnvVarUpdate
import pydantic
with pytest.raises(pydantic.ValidationError):
EnvVarUpdate(key="SOME_KEY", value="")
def test_rejects_whitespace_only_value(self):
from hermes_cli.web_server import EnvVarUpdate
import pydantic
with pytest.raises(pydantic.ValidationError):
EnvVarUpdate(key="SOME_KEY", value=" ")
def test_accepts_nonempty_value(self):
from hermes_cli.web_server import EnvVarUpdate
update = EnvVarUpdate(key="SOME_KEY", value="sk-abc123")
assert update.value == "sk-abc123"
def test_rejects_empty_key(self):
from hermes_cli.web_server import EnvVarUpdate
import pydantic
with pytest.raises(pydantic.ValidationError):
EnvVarUpdate(key="", value="some-value")