Release v0.51.263 — Release IE (stage-r13) (#3625)

## Release v0.51.263 — Release IE (stage-r13)

Batch 1 (fresh) — trimmed to the clean pair after the gate held two.

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3621 | @luanxu-dev | `/codex-runtime` + `/codex_runtime` now run as a WebUI slash command (routed through the executor reusing the agent's `codex_runtime_switch`) instead of being sent to the model as a chat message. |

### Tests
| Issue/PR | Author | Change |
|----------|--------|--------|
| #3595 | @rodboev | Regression coverage for the already-shipped `activity_feed_expanded_default` setting. |

### Held back from this batch (Codex regression gate)
- **#3624** (passkey-challenge cap, security) — the cap **raises** `PasskeyRateLimitError` when full instead of **evicting oldest**, so an attacker (or 8 abandoned legit attempts per context) can lock out genuine registration/login until TTL — the protection becomes a lockout DoS. Held with the oldest-first-eviction fix.
- **#3618** (prefer server-side STT) — forcing MediaRecorder by default breaks browser `SpeechRecognition` dictation on installs with **no** server STT configured (`_transcribeBlob` only toasts on failure, never falls back). Held with the graceful-fallback fix.

### Gate
- Full pytest suite: **7714 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (#3624 DoS-lockout + #3618 STT-no-fallback) → both dropped/held → **SAFE TO SHIP** (verified no passkeys.py/boot.js remnants, codex-runtime dispatch reaches the allowlist).

Co-authored-by: luanxu-dev <luanxu-dev@users.noreply.github.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
This commit is contained in:
nesquena-hermes 2026-06-04 23:56:44 -07:00 committed by GitHub
parent 163df17410
commit 52261ebdaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 248 additions and 8 deletions

View file

@ -3,6 +3,14 @@
## [Unreleased]
## [v0.51.263] — 2026-06-04 — Release IE (stage-r13 — codex-runtime slash command + activity-default test)
### Fixed
- **`/codex-runtime` (and `/codex_runtime`) now run as a WebUI slash command** instead of being sent to the model as a normal chat message — routed through the slash-command executor reusing the agent's shared `codex_runtime_switch` parser/apply logic. (#3621, @luanxu-dev)
### Tests
- Added regression coverage for the already-shipped `activity_feed_expanded_default` setting (default Activity-group expansion). (#3595, @rodboev)
## [v0.51.262] — 2026-06-04 — Release ID (stage-r12 — zh localization + Docker reveal-path + recall-prefill role fix)
### Fixed

View file

@ -24,13 +24,15 @@ _NEVER_EXPOSE: frozenset[str] = frozenset({
# Narrow agent-side execution allowlist for /api/commands/exec.
_AGENT_COMMAND_ALIASES = {
'reload_mcp': 'reload-mcp',
'codex_runtime': 'codex-runtime',
}
_ALLOWED_AGENT_COMMANDS = frozenset({'reload-mcp'})
_ALLOWED_AGENT_COMMANDS = frozenset({'reload-mcp', 'codex-runtime'})
_RELOAD_MCP_LOCK = threading.Lock()
_CODEX_RUNTIME_LOCK = threading.Lock()
def _normalize_agent_command_name(command: str) -> str:
"""Normalize slash text to a canonical command name."""
def _parse_agent_command(command: str) -> tuple[str, str]:
"""Return ``(canonical_name, arg_string)`` from slash-command text."""
raw = str(command or "").strip()
if not raw:
@ -42,7 +44,14 @@ def _normalize_agent_command_name(command: str) -> str:
if not cmd_base:
raise ValueError("command is required")
return _AGENT_COMMAND_ALIASES.get(cmd_base, cmd_base)
return _AGENT_COMMAND_ALIASES.get(cmd_base, cmd_base), cmd_parts[1] if len(cmd_parts) > 1 else ""
def _normalize_agent_command_name(command: str) -> str:
"""Normalize slash text to a canonical command name."""
canonical, _arg_string = _parse_agent_command(command)
return canonical
def list_commands(_registry=None) -> list[dict[str, Any]]:
@ -105,16 +114,51 @@ def list_commands(_registry=None) -> list[dict[str, Any]]:
def execute_agent_command(command: str) -> str:
"""Execute a narrow allowlist of agent-side runtime commands."""
canonical = _normalize_agent_command_name(command)
canonical, arg_string = _parse_agent_command(command)
if canonical not in _ALLOWED_AGENT_COMMANDS:
raise KeyError(canonical)
if canonical == 'reload-mcp':
return _run_reload_mcp_command()
if canonical == 'codex-runtime':
return _run_codex_runtime_command(arg_string)
raise KeyError(canonical)
def _run_codex_runtime_command(arg_string: str) -> str:
"""Execute Hermes' shared Codex runtime switch for the active profile."""
try:
from hermes_cli.codex_runtime_switch import apply, parse_args
except Exception as exc:
logger.warning("Codex runtime switch unavailable", exc_info=True)
raise RuntimeError("Codex runtime switch unavailable") from exc
new_value, errors = parse_args(arg_string)
if errors:
return "\n".join(str(error) for error in errors)
with _CODEX_RUNTIME_LOCK:
try:
from api import config as webui_config
active_config = webui_config.get_config()
def _persist_config(config_data: dict) -> None:
webui_config._save_yaml_config_file(
webui_config._get_config_path(),
config_data,
)
webui_config.reload_config()
status = apply(active_config, new_value, persist_callback=_persist_config)
except Exception as exc:
logger.warning("Failed to execute /codex-runtime", exc_info=True)
raise RuntimeError("Failed to update Codex runtime") from exc
return str(getattr(status, "message", "") or "(no output)")
def _run_reload_mcp_command() -> str:
"""Execute the MCP reconnect path and return a short user-facing summary."""
with _RELOAD_MCP_LOCK:

View file

@ -300,7 +300,7 @@ const _sessionTitleProvisionalBySid = new Map();
// their canonical command is registered on the backend (for example
// /reload-mcp). Keep this intentionally narrow and include underscore variants
// observed by users so typing either form still routes through executeAgentCommand.
const _AGENT_COMMANDS_RUN_ON_WEBUI = new Set(['reload-mcp', 'reload_mcp']);
const _AGENT_COMMANDS_RUN_ON_WEBUI = new Set(['reload-mcp', 'reload_mcp', 'codex-runtime', 'codex_runtime']);
function _clearStaleBusyStateBeforeSend({compressionRunning=false}={}){
if(!S||!S.busy||compressionRunning) return false;

View file

@ -106,6 +106,13 @@ def _run_commands_js(script_body: str) -> dict:
aliases: [],
cli_only: false,
gateway_only: false
}},
{{
name: 'codex-runtime',
description: 'Toggle Codex app-server runtime',
aliases: ['codex_runtime'],
cli_only: false,
gateway_only: false
}}
]
}};
@ -188,12 +195,34 @@ def test_send_intercepts_reload_mcp_agent_command_before_agent_round_trip():
assert "executeAgentCommand(text,_agentCmd||{name:_agentCmdName})" in intercept
def test_reload_mcp_webui_intercept_aliases_are_defined_in_js_whitelist():
def test_reload_mcp_and_codex_runtime_webui_intercept_aliases_are_defined_in_js_whitelist():
assert "'reload-mcp'" in MESSAGES_JS
assert "'reload_mcp'" in MESSAGES_JS
assert "'codex-runtime'" in MESSAGES_JS
assert "'codex_runtime'" in MESSAGES_JS
assert "if(_agentCmd&&_AGENT_COMMANDS_RUN_ON_WEBUI.has(_agentCmdName))" not in MESSAGES_JS
def test_codex_runtime_agent_command_metadata_resolves_alias():
result = _run_commands_js(
"""
const byName = await getAgentCommandMetadata('codex-runtime');
const byAlias = await getAgentCommandMetadata('codex_runtime');
return {
by_name: byName && byName.name,
by_alias: byAlias && byAlias.name,
cli_only: byAlias && byAlias.cli_only === true
};
"""
)
assert result == {
"by_name": "codex-runtime",
"by_alias": "codex-runtime",
"cli_only": False,
}
def test_unknown_slash_commands_still_fall_through_to_agent():
"""Only explicitly supported metadata-backed commands should be intercepted."""
intercept_idx = MESSAGES_JS.find("Slash command intercept")

View file

@ -4,7 +4,8 @@ import urllib.error
import urllib.request
import threading
import time
from types import ModuleType
from types import ModuleType, SimpleNamespace
from typing import Any, cast
import pytest
@ -25,6 +26,40 @@ def _install_fake_mcp_tool(monkeypatch, shutdown, discover, servers=None, lock=N
return mcp_tool
def _install_fake_codex_runtime_switch(monkeypatch):
import sys
hermes_cli_pkg = sys.modules.get("hermes_cli") or ModuleType("hermes_cli")
hermes_cli_pkg.__path__ = []
codex_runtime_switch = ModuleType("hermes_cli.codex_runtime_switch")
calls = []
def parse_args(arg_string):
calls.append(("parse_args", arg_string))
if arg_string in ("on", "codex_app_server"):
return "codex_app_server", []
if arg_string in ("", None):
return None, []
return None, [f"bad arg: {arg_string}"]
def apply(config, new_value, *, persist_callback=None):
calls.append(("apply", new_value, config.get("model", {}).get("openai_runtime")))
if new_value is not None:
config.setdefault("model", {})["openai_runtime"] = new_value
if persist_callback:
persist_callback(config)
return SimpleNamespace(
success=True,
message=f"codex runtime -> {new_value or config.get('model', {}).get('openai_runtime', 'auto')}",
)
codex_runtime_switch_any = cast(Any, codex_runtime_switch)
codex_runtime_switch_any.parse_args = parse_args
codex_runtime_switch_any.apply = apply
monkeypatch.setitem(sys.modules, "hermes_cli", hermes_cli_pkg)
monkeypatch.setitem(sys.modules, "hermes_cli.codex_runtime_switch", codex_runtime_switch)
return calls
def _get(path):
"""GET helper -- returns parsed JSON or raises HTTPError."""
with urllib.request.urlopen(TEST_BASE + path, timeout=10) as r:
@ -118,6 +153,66 @@ def test_commands_exec_runs_reload_mcp_alias():
assert isinstance(body['output'], str)
def test_codex_runtime_command_uses_shared_switch_and_persists(monkeypatch, tmp_path):
"""`/codex-runtime` executes through the same shared switch as CLI/gateway."""
calls = _install_fake_codex_runtime_switch(monkeypatch)
saved = []
from api import config as webui_config
from api.commands import execute_agent_command
config_data = {"model": {"openai_runtime": "auto"}}
monkeypatch.setattr(webui_config, "get_config", lambda: config_data)
monkeypatch.setattr(webui_config, "_get_config_path", lambda: tmp_path / "config.yaml")
monkeypatch.setattr(
webui_config,
"_save_yaml_config_file",
lambda path, data: saved.append((path, data.copy())),
)
monkeypatch.setattr(webui_config, "reload_config", lambda: saved.append(("reload", None)))
output = execute_agent_command('/codex-runtime on')
assert output == "codex runtime -> codex_app_server"
assert config_data["model"]["openai_runtime"] == "codex_app_server"
assert calls == [
("parse_args", "on"),
("apply", "codex_app_server", "auto"),
]
assert saved[0][0] == tmp_path / "config.yaml"
assert saved[0][1] == {"model": {"openai_runtime": "codex_app_server"}}
assert saved[1] == ("reload", None)
def test_codex_runtime_command_accepts_underscore_alias(monkeypatch):
"""Telegram/WebUI underscore spelling routes to the canonical command."""
calls = _install_fake_codex_runtime_switch(monkeypatch)
from api import config as webui_config
from api.commands import execute_agent_command
monkeypatch.setattr(webui_config, "get_config", lambda: {"model": {"openai_runtime": "auto"}})
monkeypatch.setattr(webui_config, "_save_yaml_config_file", lambda path, data: None)
monkeypatch.setattr(webui_config, "reload_config", lambda: None)
output = execute_agent_command('/codex_runtime codex_app_server')
assert output == "codex runtime -> codex_app_server"
assert calls[0] == ("parse_args", "codex_app_server")
def test_codex_runtime_invalid_argument_returns_switch_message(monkeypatch):
"""Argument validation stays in the shared switch and returns user text."""
calls = _install_fake_codex_runtime_switch(monkeypatch)
from api.commands import execute_agent_command
output = execute_agent_command('/codex-runtime nope')
assert output == "bad arg: nope"
assert calls == [("parse_args", "nope")]
def test_reload_mcp_error_is_generic(monkeypatch):
"""`/reload-mcp` errors must return a generic message, not raw internals."""
calls = []

View file

@ -0,0 +1,64 @@
"""Pin the full behavioral contract for the Activity expanded-default setting."""
import pathlib
import re
ROOT = pathlib.Path(__file__).resolve().parent.parent
def test_setting_in_defaults():
src = (ROOT / "api" / "config.py").read_text(encoding="utf-8")
assert '"activity_feed_expanded_default"' in src or "'activity_feed_expanded_default'" in src, \
"activity_feed_expanded_default must exist in _SETTINGS_DEFAULTS"
# Verify default is False
assert re.search(r'["\']activity_feed_expanded_default["\']:\s*False', src), \
"activity_feed_expanded_default default must be False (collapsed)"
def test_setting_in_bool_keys():
src = (ROOT / "api" / "config.py").read_text(encoding="utf-8")
assert re.search(r'_SETTINGS_BOOL_KEYS\b.*?activity_feed_expanded_default', src, re.DOTALL), \
"activity_feed_expanded_default must appear inside _SETTINGS_BOOL_KEYS (not just anywhere in config.py)"
def test_boot_initializes_window_flag():
src = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
assert "activityFeedExpandedDefault" in src, \
"boot.js must initialize window._activityFeedExpandedDefault from settings"
def test_ensure_activity_group_checks_flag():
src = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
assert "_activityFeedExpandedDefault" in src, \
"ensureActivityGroup must check window._activityFeedExpandedDefault"
def test_per_turn_override():
src = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
assert re.search(r"savedState\s*===\s*['\"]closed['\"]", src), \
"ensureActivityGroup must check savedState==='closed' so per-turn persistence overrides the global default"
def test_collapse_class_applied():
src = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
assert "tool-call-group-collapsed" in src, \
"The collapsed class 'tool-call-group-collapsed' must be present in ui.js for the activity group"
def test_settings_checkbox_exists():
src = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
assert "settings_label_activity_feed_expanded_default" in src, \
"index.html must have a settings checkbox with data-i18n for activity_feed_expanded_default"
def test_panels_wiring():
src = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
assert "activity_feed_expanded_default" in src, \
"panels.js must read/write the activity_feed_expanded_default setting"
def test_i18n_keys():
src = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
assert "settings_label_activity_feed_expanded_default" in src, \
"i18n.js must have the label key for the setting"
assert "settings_desc_activity_feed_expanded_default" in src, \
"i18n.js must have the description key for the setting"