Changed dashboard listen address to 127.0.0.1

Also minor refactoring in SerenaConfig that allows not repeating the defaults (as field defaults and in `from_config_file`)
This commit is contained in:
Michael Panchenko authored and Dominik Jain committed 2025-12-10 12:03:16 +01:00
1 parent 904a25053e
commit 7bef5a6ee3
4 files changed
+51 -20

No files matched your search

+6 -2
View File
@@ -271,8 +271,12 @@ class SerenaAgent:
if self.serena_config.web_dashboard:
self._dashboard_thread, port = SerenaDashboardAPI(
get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats
).run_in_thread()
dashboard_url = f"http://127.0.0.1:{port}/dashboard/index.html"
).run_in_thread(host=self.serena_config.web_dashboard_host)
host_for_url = self.serena_config.web_dashboard_host
# this usually means we are in docker, so we use localhost for URL (the user has to port-forward then)
if host_for_url == "0.0.0.0":
host_for_url = "127.0.0.1"
dashboard_url = f"http://{self.serena_config.web_dashboard_host}:{port}/dashboard/index.html"
log.info("Serena web dashboard started at %s", dashboard_url)
if self.serena_config.web_dashboard_open_on_launch:
# open the dashboard URL in the default web browser (using a separate process to control
+21 -14
View File
@@ -28,7 +28,7 @@ from serena.constants import (
SERENA_MANAGED_DIR_IN_HOME,
SERENA_MANAGED_DIR_NAME,
)
from serena.util.general import load_yaml, save_yaml
from serena.util.general import get_dataclass_default, load_yaml, save_yaml
from serena.util.inspection import determine_programming_language_composition
from solidlsp.ls_config import Language
@@ -342,6 +342,8 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
trace_lsp_communication: bool = False
web_dashboard: bool = True
web_dashboard_open_on_launch: bool = True
web_dashboard_host: str = "127.0.0.1"
"""Will be automatically set to '0.0.0.0' when running in Docker. Not loaded from config file."""
tool_timeout: float = DEFAULT_TOOL_TIMEOUT
loaded_commented_yaml: CommentedMap | None = None
config_file_path: str | None = None
@@ -373,6 +375,10 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
CONFIG_FILE = "serena_config.yml"
CONFIG_FILE_DOCKER = "serena_config.docker.yml" # Docker-specific config file; auto-generated if missing, mounted via docker-compose for user customization
def __post_init__(self) -> None:
if is_running_in_docker():
self.web_dashboard_host = "0.0.0.0"
def _tostring_includes(self) -> list[str]:
return ["config_file_path"]
@@ -456,23 +462,24 @@ class SerenaConfig(ToolInclusionDefinition, ToStringMixin):
instance.projects.append(project)
# set other configuration parameters
def get_value_or_default(field_name: str) -> Any:
return loaded_commented_yaml.get(field_name, get_dataclass_default(SerenaConfig, field_name))
if is_running_in_docker():
instance.gui_log_window_enabled = False # not supported in Docker
else:
instance.gui_log_window_enabled = loaded_commented_yaml.get("gui_log_window", False)
instance.gui_log_window_enabled = get_value_or_default("gui_log_window_enabled")
instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO))
instance.web_dashboard = loaded_commented_yaml.get("web_dashboard", True)
instance.web_dashboard_open_on_launch = loaded_commented_yaml.get("web_dashboard_open_on_launch", True)
instance.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT)
instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False)
instance.excluded_tools = loaded_commented_yaml.get("excluded_tools", [])
instance.included_optional_tools = loaded_commented_yaml.get("included_optional_tools", [])
instance.jetbrains = loaded_commented_yaml.get("jetbrains", False)
instance.token_count_estimator = loaded_commented_yaml.get(
"token_count_estimator", RegisteredTokenCountEstimator.TIKTOKEN_GPT4O.name
)
instance.default_max_tool_answer_chars = loaded_commented_yaml.get("default_max_tool_answer_chars", 150_000)
instance.ls_specific_settings = loaded_commented_yaml.get("ls_specific_settings", {})
instance.web_dashboard = get_value_or_default("web_dashboard")
instance.web_dashboard_open_on_launch = get_value_or_default("web_dashboard_open_on_launch")
instance.tool_timeout = get_value_or_default("tool_timeout")
instance.trace_lsp_communication = get_value_or_default("trace_lsp_communication")
instance.excluded_tools = get_value_or_default("excluded_tools")
instance.included_optional_tools = get_value_or_default("included_optional_tools")
instance.jetbrains = get_value_or_default("jetbrains")
instance.token_count_estimator = get_value_or_default("token_count_estimator")
instance.default_max_tool_answer_chars = get_value_or_default("default_max_tool_answer_chars")
instance.ls_specific_settings = get_value_or_default("ls_specific_settings")
# re-save the configuration file if any migrations were performed
if num_project_migrations > 0:
+3 -3
View File
@@ -560,7 +560,7 @@ class SerenaDashboardAPI:
raise RuntimeError(f"No free ports found starting from {start_port}")
def run(self, host: str = "0.0.0.0", port: int = 0x5EDA) -> int:
def run(self, host: str, port: int) -> int:
"""
Runs the dashboard on the given host and port and returns the port number.
"""
@@ -572,8 +572,8 @@ class SerenaDashboardAPI:
self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True)
return port
def run_in_thread(self) -> tuple[threading.Thread, int]:
def run_in_thread(self, host: str) -> tuple[threading.Thread, int]:
port = self._find_first_free_port(0x5EDA)
thread = threading.Thread(target=lambda: self.run(port=port), daemon=True)
thread = threading.Thread(target=lambda: self.run(host=host, port=port), daemon=True)
thread.start()
return thread, port
+21 -1
View File
@@ -1,5 +1,6 @@
import os
from typing import Literal, overload
from dataclasses import MISSING, Field
from typing import Any, Literal, cast, overload
from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap
@@ -32,3 +33,22 @@ def save_yaml(path: str, data: dict | CommentedMap, preserve_comments: bool = Fa
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding=SERENA_FILE_ENCODING) as f:
yaml.dump(data, f)
def get_dataclass_default(cls: type, field_name: str) -> Any:
"""
Gets the default value of a dataclass field.
:param cls: The dataclass type.
:param field_name: The name of the field.
:return: The default value of the field (either from default or default_factory).
"""
field = cast(Field, cls.__dataclass_fields__[field_name]) # type: ignore[attr-defined]
if field.default is not MISSING:
return field.default
if field.default_factory is not MISSING: # default_factory is a function
return field.default_factory()
raise AttributeError(f"{field_name} has no default")