Skip to content

Commit 33b3388

Browse files
committed
Prevent global command settings mutation from auto-registered commands
1 parent 7357848 commit 33b3388

File tree

2 files changed

+88
-33
lines changed

2 files changed

+88
-33
lines changed

src/core/app/stages/command.py

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,64 @@
1010
from __future__ import annotations
1111

1212
import logging
13+
from typing import Any
1314

1415
from src.core.config.app_config import AppConfig
1516
from src.core.di.container import ServiceCollection
1617
from src.core.interfaces.application_state_interface import IApplicationState
18+
from src.core.interfaces.command_settings_interface import ICommandSettingsService
1719
from src.core.interfaces.di_interface import IServiceProvider
20+
from src.core.interfaces.state_provider_interface import (
21+
ISecureStateAccess,
22+
ISecureStateModification,
23+
)
1824

1925
from .base import InitializationStage
2026

2127
logger = logging.getLogger(__name__)
2228

2329

30+
class DefaultCommandStateService(ISecureStateAccess, ISecureStateModification):
31+
"""Lightweight state holder used when auto-registering domain commands."""
32+
33+
def __init__(self, settings_service: ICommandSettingsService) -> None:
34+
self._settings = settings_service
35+
self._routes: list[dict[str, Any]] = []
36+
self._command_prefix_override: str | None = None
37+
self._api_key_redaction_override: bool | None = None
38+
self._disable_interactive_override: bool | None = None
39+
40+
def get_command_prefix(self) -> str:
41+
if self._command_prefix_override is not None:
42+
return self._command_prefix_override
43+
return self._settings.get_command_prefix()
44+
45+
def get_failover_routes(self) -> list[dict[str, Any]] | None:
46+
return self._routes
47+
48+
def update_failover_routes(self, routes: list[dict[str, Any]]) -> None:
49+
self._routes = routes
50+
51+
def get_api_key_redaction_enabled(self) -> bool:
52+
if self._api_key_redaction_override is not None:
53+
return self._api_key_redaction_override
54+
return self._settings.get_api_key_redaction_enabled()
55+
56+
def get_disable_interactive_commands(self) -> bool:
57+
if self._disable_interactive_override is not None:
58+
return self._disable_interactive_override
59+
return self._settings.get_disable_interactive_commands()
60+
61+
def update_command_prefix(self, prefix: str) -> None:
62+
if isinstance(prefix, str) and prefix:
63+
self._command_prefix_override = prefix
64+
65+
def update_api_key_redaction(self, enabled: bool) -> None:
66+
self._api_key_redaction_override = bool(enabled)
67+
68+
def update_interactive_commands(self, enabled: bool) -> None:
69+
self._disable_interactive_override = bool(enabled)
70+
2471
class CommandStage(InitializationStage):
2572
"""
2673
Stage for registering command-related services.
@@ -188,39 +235,7 @@ def populate_commands_factory(provider: IServiceProvider) -> None:
188235
ICommandSettingsService # type: ignore[type-abstract]
189236
)
190237

191-
# Create a simple state service for commands
192-
class DefaultStateService(
193-
ISecureStateAccess, ISecureStateModification
194-
):
195-
def __init__(self, settings_service):
196-
self._settings = settings_service
197-
self._routes = []
198-
199-
def get_command_prefix(self):
200-
return self._settings.get_command_prefix()
201-
202-
def get_failover_routes(self):
203-
return self._routes
204-
205-
def update_failover_routes(self, routes):
206-
self._routes = routes
207-
208-
def get_api_key_redaction_enabled(self):
209-
return self._settings.get_api_key_redaction_enabled()
210-
211-
def get_disable_interactive_commands(self):
212-
return self._settings.get_disable_interactive_commands()
213-
214-
def update_command_prefix(self, prefix: str) -> None:
215-
self._settings.command_prefix = prefix
216-
217-
def update_api_key_redaction(self, enabled: bool) -> None:
218-
self._settings.api_key_redaction_enabled = enabled
219-
220-
def update_interactive_commands(self, enabled: bool) -> None:
221-
pass
222-
223-
state_service = DefaultStateService(settings_service)
238+
state_service = DefaultCommandStateService(settings_service)
224239

225240
# Auto-register all commands from the domain command registry
226241
for (
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from src.core.app.stages.command import DefaultCommandStateService
2+
from src.core.services.command_settings_service import CommandSettingsService
3+
4+
5+
def _make_settings() -> CommandSettingsService:
6+
return CommandSettingsService(
7+
default_command_prefix="!/",
8+
default_api_key_redaction=True,
9+
default_disable_interactive_commands=False,
10+
)
11+
12+
13+
def test_command_prefix_override_is_session_local() -> None:
14+
settings = _make_settings()
15+
state_service = DefaultCommandStateService(settings)
16+
17+
state_service.update_command_prefix("$/")
18+
19+
assert state_service.get_command_prefix() == "$/"
20+
assert settings.get_command_prefix() == "!/"
21+
22+
23+
def test_api_key_redaction_override_is_session_local() -> None:
24+
settings = _make_settings()
25+
state_service = DefaultCommandStateService(settings)
26+
27+
state_service.update_api_key_redaction(False)
28+
29+
assert state_service.get_api_key_redaction_enabled() is False
30+
assert settings.get_api_key_redaction_enabled() is True
31+
32+
33+
def test_disable_interactive_commands_override_is_session_local() -> None:
34+
settings = _make_settings()
35+
state_service = DefaultCommandStateService(settings)
36+
37+
state_service.update_interactive_commands(True)
38+
39+
assert state_service.get_disable_interactive_commands() is True
40+
assert settings.get_disable_interactive_commands() is False

0 commit comments

Comments
 (0)