From 5a2063fc3a39575cb54769451ac5a49fd20b6f79 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 17 Sep 2026 12:48:53 +0200 Subject: [PATCH] Add a persistent authentication secret to SerenaConfig - Generate a random UUID when auth_secret is missing, null, or empty. - Persist generated secrets and preserve configured values across reloads. - Document the shared secret and test generation, persistence, and direct construction. --- CHANGELOG.md | 3 ++ src/serena/config/serena_config.py | 11 +++++++ .../resources/serena_config.template.yml | 4 +++ test/serena/config/test_serena_config.py | 31 +++++++++++++++++++ 4 files changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aeb6dbc..3d3c13f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ Status of the `main` branch. Changes prior to the next official version change w see `CONTRIBUTING.md` * General: + - Add `auth_secret` to `serena_config.yml` for authenticating communication between Serena components + and services. When missing, null, or empty, a random UUID is generated and persisted; existing values + are preserved - Fix: MCP `initialize` now reports Serena's version instead of the installed mcp SDK version (#1889) - Fix: Parallel agents auto-registering projects could overwrite each other's changes to the global project list in `serena_config.yml` diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 60cd5ec6..de47c016 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -16,6 +16,7 @@ from enum import Enum from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Self, TypeVar +from uuid import uuid4 import yaml from ruamel.yaml.comments import CommentedMap @@ -945,6 +946,11 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): # *** fields that are mapped directly to/from the configuration file (DO NOT RENAME) *** projects: list[RegisteredProject] = field(default_factory=list) + auth_secret: str = field(default_factory=lambda: str(uuid4()), repr=False) + """ + shared secret for authenticating communication between Serena components and services. + A random UUID is generated and persisted when the configuration setting is missing or empty. + """ gui_log_window: bool = False log_level: int = logging.INFO trace_lsp_communication: bool = False @@ -1138,6 +1144,11 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes): assert hasattr(instance, field_name) setattr(instance, field_name, get_value_or_default(field_name)) + # generate a persistent authentication secret for explicitly unset settings + if not instance.auth_secret: + instance.auth_secret = str(uuid4()) + num_migrations += 1 + # read projects if "projects" not in loaded_commented_yaml: raise SerenaConfigError("`projects` key not found in Serena configuration. Please update your `serena_config.yml` file.") diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index ea8e3d17..29485b3e 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -235,5 +235,9 @@ project_serena_folder_location: "$projectDir/.serena" # The pattern "**" matches any project path, so it can be used to trust all projects. trusted_project_path_patterns: [] +# shared secret for authenticating communication between Serena components and services. +# Keep this value private. When missing, null, or empty, a random UUID is generated and saved on load. +auth_secret: + # the list of registered project paths (updated automatically). projects: [] diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index 64b7a852..4d05f615 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -4,6 +4,7 @@ import shutil import tempfile from copy import deepcopy from pathlib import Path +from uuid import UUID import pytest @@ -635,6 +636,36 @@ class TestSerenaConfigLoadSave: config = SerenaConfig.from_config_file(generate_if_missing=False) assert config.projects == [] + @pytest.mark.parametrize("setting", ["", "auth_secret: null\n", 'auth_secret: ""\n']) + def test_unset_auth_secret_is_generated_and_persisted(self, setting: str) -> None: + # load an existing configuration without a usable secret + self.master_config_path.write_text("projects: []\n" + setting) + config = SerenaConfig.from_config_file(generate_if_missing=False) + + # subsequent loads retain the generated random UUID + assert UUID(config.auth_secret).version == 4 + assert SerenaConfig.from_config_file(generate_if_missing=False).auth_secret == config.auth_secret + + def test_configured_auth_secret_is_preserved(self) -> None: + # retain a user-provided secret across loading and migration + self.master_config_path.write_text("projects: []\nauth_secret: custom-secret\n") + assert SerenaConfig.from_config_file(generate_if_missing=False).auth_secret == "custom-secret" + assert SerenaConfig.from_config_file(generate_if_missing=False).auth_secret == "custom-secret" + + def test_new_config_has_persistent_auth_secret(self) -> None: + # generate the configuration from the template and retain its secret + config = SerenaConfig.from_config_file() + assert UUID(config.auth_secret).version == 4 + assert SerenaConfig.from_config_file().auth_secret == config.auth_secret + + def test_direct_config_instances_have_distinct_auth_secrets(self) -> None: + # directly constructed configurations receive independent secrets + first = SerenaConfig() + second = SerenaConfig() + assert UUID(first.auth_secret).version == 4 + assert UUID(second.auth_secret).version == 4 + assert first.auth_secret != second.auth_secret + def test_malformed_project_is_skipped_with_warning(self, caplog): """A malformed project.yml must not abort loading of the others.""" good_project = self._make_project_dir(