diff --git a/docs/read_only_mode.md b/docs/read_only_mode.md deleted file mode 100644 index 422572b8..00000000 --- a/docs/read_only_mode.md +++ /dev/null @@ -1,46 +0,0 @@ -# Read-Only Mode - -Serena supports a read-only mode that prevents any modifications to the codebase while still allowing analysis and exploration. This feature is useful for: - -- Reviewing and analyzing code without the risk of accidental modifications -- Allowing junior developers to explore a codebase with AI assistance but without modification privileges -- Generating documentation or explanations without changing the underlying code -- Creating proof-of-concept designs without implementing them - -## Enabling Read-Only Mode - -To enable read-only mode, set the `read_only` flag to `true` in your project configuration file: - -```yaml -# whether the project is in read-only mode -read_only: true -``` - -## How It Works - -When read-only mode is enabled: - -1. All tools that perform editing operations are automatically disabled -2. Any attempt to use an editing tool will result in an error message -3. All non-editing tools remain fully functional - -The editing tools that get disabled include: -- `create_text_file`: Creating or overwriting files -- `replace_symbol_body`: Replacing code symbol definitions -- `insert_after_symbol`: Inserting code after symbols -- `insert_before_symbol`: Inserting code before symbols -- `delete_lines`: Deleting lines from files -- `replace_lines`: Replacing lines in files -- `insert_at_line`: Inserting content at specific lines -- `execute_shell_command`: Executing shell commands that could potentially modify files -- `insert_at_line`: Inserting content at specific lines - -## Implementation Details - -Serena uses the `EditingTool` marker class to identify tools that can modify code. The base `Tool` class includes a `can_edit()` method that checks if a tool is a subclass of `EditingTool`. - -When a project is configured in read-only mode: -1. During project activation, all editing tools are automatically excluded from the active tools list -2. An additional runtime check prevents any editing tools from being executed, even if they somehow remain in the active tools list - -This ensures that no modifications to the codebase can occur while in read-only mode. diff --git a/src/serena/agent.py b/src/serena/agent.py index 207f4c78..83580f53 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -31,7 +31,6 @@ from serena.text_utils import search_files from serena.util.class_decorators import singleton from serena.util.file_system import scan_directory from serena.util.inspection import iter_subclasses -from serena.editingtool import EditingTool from serena.util.shell import execute_shell_command if TYPE_CHECKING: @@ -117,7 +116,6 @@ class ProjectConfig(ToStringMixin): return os.path.join(self.project_root, self.SERENA_MANAGED_DIR) - @singleton class SerenaConfig: """ @@ -269,7 +267,6 @@ class SerenaAgent: # When project activation is not enabled, we only expose the active tools return list(self._active_tools.values()) - def activate_project(self, project_config: ProjectConfig) -> None: log.info(f"Activating {project_config}") self.project_config = project_config @@ -282,13 +279,13 @@ class SerenaAgent: log.info(f"Active tools after exclusions ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}") else: self._active_tools = dict(self._all_tools) - + # if read_only mode is enabled, exclude all editing tools if self.project_config.read_only: - self._active_tools = { - key: tool for key, tool in self._active_tools.items() if not key.can_edit() - } - log.info(f"Project is in read-only mode. Editing tools excluded. Active tools ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}") + self._active_tools = {key: tool for key, tool in self._active_tools.items() if not key.can_edit()} + log.info( + f"Project is in read-only mode. Editing tools excluded. Active tools ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}" + ) # start the language server self.reset_language_server() @@ -302,7 +299,6 @@ class SerenaAgent: if self._project_activation_callback is not None: self._project_activation_callback() - def get_active_tool_names(self) -> list[str]: """ :return: the list of names of the active tools for the current project @@ -436,6 +432,16 @@ class Component(ABC): _DEFAULT_MAX_ANSWER_LENGTH = int(2e5) +class ToolMarkerCanEdit: + """ + Marker class for all tools that can perform editing operations on files. + """ + + +class ToolMarkerDoesNotRequireActiveProject: + pass + + class Tool(Component): # NOTE: each tool should implement the apply method, which is then used in # the central method of the Tool class `apply_ex`. @@ -461,15 +467,15 @@ class Tool(Component): if apply_fn is None: raise RuntimeError(f"apply not defined in {self}. Did you forget to implement it?") return apply_fn - + @classmethod def can_edit(cls) -> bool: """ Returns whether this tool can perform editing operations on code. - + :return: True if the tool can edit code, False otherwise """ - return issubclass(cls, EditingTool) + return issubclass(cls, ToolMarkerCanEdit) @classmethod def get_tool_description(cls) -> str: @@ -533,13 +539,9 @@ class Tool(Component): f"Error: Tool '{self.get_name()}' is disabled for the active project ('{self.project_config.project_name}'); " f"active tools: {self.agent.get_active_tool_names()}" ) - + # check if the project is in read-only mode and this is an editing tool - if ( - self.agent.project_config is not None - and self.agent.project_config.read_only - and self.__class__.can_edit() - ): + if self.agent.project_config is not None and self.agent.project_config.read_only and self.__class__.can_edit(): return ( f"Error: Tool '{self.get_name()}' cannot be used because the project '{self.project_config.project_name}' " f"is in read-only mode. Editing operations are not allowed." @@ -561,11 +563,6 @@ class Tool(Component): return result - -class ToolMarkerDoesNotRequireActiveProject: - pass - - class RestartLanguageServerTool(Tool): """Restarts the language server, may be necessary when edits not through Serena happen.""" @@ -613,7 +610,7 @@ class ReadFileTool(Tool): return self._limit_length(result, max_answer_chars) -class CreateTextFileTool(Tool, EditingTool): +class CreateTextFileTool(Tool, ToolMarkerCanEdit): """ Creates/overwrites a file in the project directory. """ @@ -865,7 +862,7 @@ class FindReferencingCodeSnippetsTool(Tool): return self._limit_length(result_json_str, max_answer_chars) -class ReplaceSymbolBodyTool(Tool, EditingTool): +class ReplaceSymbolBodyTool(Tool, ToolMarkerCanEdit): """ Replaces the full definition of a symbol. """ @@ -894,7 +891,7 @@ class ReplaceSymbolBodyTool(Tool, EditingTool): return SUCCESS_RESULT -class InsertAfterSymbolTool(Tool, EditingTool): +class InsertAfterSymbolTool(Tool, ToolMarkerCanEdit): """ Inserts content after the end of the definition of a given symbol. """ @@ -923,7 +920,7 @@ class InsertAfterSymbolTool(Tool, EditingTool): return SUCCESS_RESULT -class InsertBeforeSymbolTool(Tool, EditingTool): +class InsertBeforeSymbolTool(Tool, ToolMarkerCanEdit): """ Inserts content before the beginning of the definition of a given symbol. """ @@ -952,7 +949,7 @@ class InsertBeforeSymbolTool(Tool, EditingTool): return SUCCESS_RESULT -class DeleteLinesTool(Tool, EditingTool): +class DeleteLinesTool(Tool, ToolMarkerCanEdit): """ Deletes a range of lines within a file. """ @@ -979,7 +976,7 @@ class DeleteLinesTool(Tool, EditingTool): return SUCCESS_RESULT -class ReplaceLinesTool(Tool, EditingTool): +class ReplaceLinesTool(Tool, ToolMarkerCanEdit): """ Replaces a range of lines within a file with new content. """ @@ -1010,7 +1007,7 @@ class ReplaceLinesTool(Tool, EditingTool): return SUCCESS_RESULT -class InsertAtLineTool(Tool, EditingTool): +class InsertAtLineTool(Tool, ToolMarkerCanEdit): """ Inserts content at a given line in a file. """ @@ -1282,7 +1279,7 @@ class SearchForPatternTool(Tool): return self._limit_length(result, max_answer_chars) -class ExecuteShellCommandTool(Tool, EditingTool): +class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit): """ Executes a shell command. """ diff --git a/src/serena/editingtool.py b/src/serena/editingtool.py deleted file mode 100644 index 341bf1c6..00000000 --- a/src/serena/editingtool.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Type - - -class EditingTool: - """ - A marker class for all tools that perform editing operations on code. - This class is used to identify tools that modify code in the project. - Tools that inherit from this class will be excluded when the project - is in read-only mode. - """ - - @classmethod - def can_edit(cls) -> bool: - """ - Returns True as editing tools are designed to modify code. - This method can be overridden by subclasses if needed. - - :return: True if the tool can edit code, False otherwise - """ - return True diff --git a/test/test_edit_marker.py b/test/test_edit_marker.py index 6e25903a..9a07f271 100644 --- a/test/test_edit_marker.py +++ b/test/test_edit_marker.py @@ -1,44 +1,13 @@ -import pytest -from serena.agent import Tool, CreateTextFileTool, ReadFileTool, ProjectConfig -from serena.editingtool import EditingTool +from serena.agent import CreateTextFileTool, ReadFileTool, Tool + class TestEditMarker: - def test_edit_marker_class(self): - """Test that EditingTool marker class exists""" - assert EditingTool.__doc__ is not None - assert hasattr(EditingTool, 'can_edit') - assert EditingTool.can_edit() is True - def test_tool_can_edit_method(self): """Test that Tool.can_edit() method works correctly""" # Non-editing tool should return False assert issubclass(ReadFileTool, Tool) - assert not issubclass(ReadFileTool, EditingTool) assert not ReadFileTool.can_edit() - + # Editing tool should return True assert issubclass(CreateTextFileTool, Tool) - assert issubclass(CreateTextFileTool, EditingTool) assert CreateTextFileTool.can_edit() - - def test_project_config_read_only(self): - """Test that ProjectConfig has read_only property""" - config = ProjectConfig({ - "language": "python", - "project_root": "/tmp", - "ignore_all_files_in_gitignore": True, - "read_only": True - }, "test_project") - - assert hasattr(config, 'read_only') - assert config.read_only is True - - config = ProjectConfig({ - "language": "python", - "project_root": "/tmp", - "ignore_all_files_in_gitignore": True, - # read_only not specified - }, "test_project") - - assert hasattr(config, 'read_only') - assert config.read_only is False # Default should be False