iflow-mcp_developermode-korea_reversecore-mcp 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/METADATA +543 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/RECORD +79 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/top_level.txt +1 -0
- reversecore_mcp/__init__.py +9 -0
- reversecore_mcp/core/__init__.py +78 -0
- reversecore_mcp/core/audit.py +101 -0
- reversecore_mcp/core/binary_cache.py +138 -0
- reversecore_mcp/core/command_spec.py +357 -0
- reversecore_mcp/core/config.py +432 -0
- reversecore_mcp/core/container.py +288 -0
- reversecore_mcp/core/decorators.py +152 -0
- reversecore_mcp/core/error_formatting.py +93 -0
- reversecore_mcp/core/error_handling.py +142 -0
- reversecore_mcp/core/evidence.py +229 -0
- reversecore_mcp/core/exceptions.py +296 -0
- reversecore_mcp/core/execution.py +240 -0
- reversecore_mcp/core/ghidra.py +642 -0
- reversecore_mcp/core/ghidra_helper.py +481 -0
- reversecore_mcp/core/ghidra_manager.py +234 -0
- reversecore_mcp/core/json_utils.py +131 -0
- reversecore_mcp/core/loader.py +73 -0
- reversecore_mcp/core/logging_config.py +206 -0
- reversecore_mcp/core/memory.py +721 -0
- reversecore_mcp/core/metrics.py +198 -0
- reversecore_mcp/core/mitre_mapper.py +365 -0
- reversecore_mcp/core/plugin.py +45 -0
- reversecore_mcp/core/r2_helpers.py +404 -0
- reversecore_mcp/core/r2_pool.py +403 -0
- reversecore_mcp/core/report_generator.py +268 -0
- reversecore_mcp/core/resilience.py +252 -0
- reversecore_mcp/core/resource_manager.py +169 -0
- reversecore_mcp/core/result.py +132 -0
- reversecore_mcp/core/security.py +213 -0
- reversecore_mcp/core/validators.py +238 -0
- reversecore_mcp/dashboard/__init__.py +221 -0
- reversecore_mcp/prompts/__init__.py +56 -0
- reversecore_mcp/prompts/common.py +24 -0
- reversecore_mcp/prompts/game.py +280 -0
- reversecore_mcp/prompts/malware.py +1219 -0
- reversecore_mcp/prompts/report.py +150 -0
- reversecore_mcp/prompts/security.py +136 -0
- reversecore_mcp/resources.py +329 -0
- reversecore_mcp/server.py +727 -0
- reversecore_mcp/tools/__init__.py +49 -0
- reversecore_mcp/tools/analysis/__init__.py +74 -0
- reversecore_mcp/tools/analysis/capa_tools.py +215 -0
- reversecore_mcp/tools/analysis/die_tools.py +180 -0
- reversecore_mcp/tools/analysis/diff_tools.py +643 -0
- reversecore_mcp/tools/analysis/lief_tools.py +272 -0
- reversecore_mcp/tools/analysis/signature_tools.py +591 -0
- reversecore_mcp/tools/analysis/static_analysis.py +479 -0
- reversecore_mcp/tools/common/__init__.py +58 -0
- reversecore_mcp/tools/common/file_operations.py +352 -0
- reversecore_mcp/tools/common/memory_tools.py +516 -0
- reversecore_mcp/tools/common/patch_explainer.py +230 -0
- reversecore_mcp/tools/common/server_tools.py +115 -0
- reversecore_mcp/tools/ghidra/__init__.py +19 -0
- reversecore_mcp/tools/ghidra/decompilation.py +975 -0
- reversecore_mcp/tools/ghidra/ghidra_tools.py +1052 -0
- reversecore_mcp/tools/malware/__init__.py +61 -0
- reversecore_mcp/tools/malware/adaptive_vaccine.py +579 -0
- reversecore_mcp/tools/malware/dormant_detector.py +756 -0
- reversecore_mcp/tools/malware/ioc_tools.py +228 -0
- reversecore_mcp/tools/malware/vulnerability_hunter.py +519 -0
- reversecore_mcp/tools/malware/yara_tools.py +214 -0
- reversecore_mcp/tools/patch_explainer.py +19 -0
- reversecore_mcp/tools/radare2/__init__.py +13 -0
- reversecore_mcp/tools/radare2/r2_analysis.py +972 -0
- reversecore_mcp/tools/radare2/r2_session.py +376 -0
- reversecore_mcp/tools/radare2/radare2_mcp_tools.py +1183 -0
- reversecore_mcp/tools/report/__init__.py +4 -0
- reversecore_mcp/tools/report/email.py +82 -0
- reversecore_mcp/tools/report/report_mcp_tools.py +344 -0
- reversecore_mcp/tools/report/report_tools.py +1076 -0
- reversecore_mcp/tools/report/session.py +194 -0
- reversecore_mcp/tools/report_tools.py +11 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Configuration management using pydantic-settings.
|
|
2
|
+
|
|
3
|
+
This module provides type-safe configuration loading from environment variables
|
|
4
|
+
with automatic validation. Code can call ``get_config()`` to access the cached
|
|
5
|
+
singleton, and tests can use ``reset_config()`` for dependency injection.
|
|
6
|
+
|
|
7
|
+
Environment Variables:
|
|
8
|
+
REVERSECORE_WORKSPACE: Path to workspace directory (default: current directory)
|
|
9
|
+
REVERSECORE_READ_DIRS: Comma-separated list of read-only directories
|
|
10
|
+
LOG_LEVEL: Logging level (default: INFO)
|
|
11
|
+
LOG_FILE: Path to log file (default: /tmp/reversecore/app.log)
|
|
12
|
+
LOG_FORMAT: Log format - "human" or "json" (default: human)
|
|
13
|
+
STRUCTURED_ERRORS: Enable structured error responses (default: false)
|
|
14
|
+
RATE_LIMIT: Rate limit per minute (default: 60)
|
|
15
|
+
LIEF_MAX_FILE_SIZE: Max file size for LIEF parsing (default: 1GB)
|
|
16
|
+
MCP_TRANSPORT: Transport mode - "stdio" or "http" (default: stdio)
|
|
17
|
+
DEFAULT_TOOL_TIMEOUT: Default timeout in seconds (default: 120)
|
|
18
|
+
R2_POOL_SIZE: Radare2 connection pool size (default: 3)
|
|
19
|
+
R2_POOL_TIMEOUT: Radare2 pool connection timeout (default: 30)
|
|
20
|
+
GHIDRA_MAX_PROJECTS: Max Ghidra projects to cache for multi-malware analysis (default: 3)
|
|
21
|
+
REVERSECORE_STRICT_PATHS: Strict path validation mode (default: false)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from pydantic import Field, field_validator, model_validator
|
|
31
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LogFormat(str, Enum):
|
|
35
|
+
"""Supported log formats."""
|
|
36
|
+
|
|
37
|
+
HUMAN = "human"
|
|
38
|
+
JSON = "json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TransportMode(str, Enum):
|
|
42
|
+
"""Supported MCP transport modes."""
|
|
43
|
+
|
|
44
|
+
STDIO = "stdio"
|
|
45
|
+
HTTP = "http"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Settings(BaseSettings):
|
|
49
|
+
"""Application settings with environment variable support.
|
|
50
|
+
|
|
51
|
+
All settings are loaded from environment variables with automatic
|
|
52
|
+
type conversion and validation.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
model_config = SettingsConfigDict(
|
|
56
|
+
env_prefix="REVERSECORE_",
|
|
57
|
+
env_file=".env",
|
|
58
|
+
env_file_encoding="utf-8",
|
|
59
|
+
extra="ignore",
|
|
60
|
+
case_sensitive=False,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Workspace configuration
|
|
64
|
+
workspace: Path = Field(
|
|
65
|
+
default_factory=Path.cwd,
|
|
66
|
+
description="Path to workspace directory for file operations",
|
|
67
|
+
)
|
|
68
|
+
read_dirs: str = Field(
|
|
69
|
+
default="",
|
|
70
|
+
alias="REVERSECORE_READ_DIRS",
|
|
71
|
+
description="Comma-separated list of read-only directories",
|
|
72
|
+
)
|
|
73
|
+
strict_paths: bool = Field(
|
|
74
|
+
default=False,
|
|
75
|
+
description="Enable strict path validation (raise errors for missing paths)",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Logging configuration
|
|
79
|
+
log_level: str = Field(
|
|
80
|
+
default="INFO",
|
|
81
|
+
alias="LOG_LEVEL",
|
|
82
|
+
description="Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL",
|
|
83
|
+
)
|
|
84
|
+
log_file: Path = Field(
|
|
85
|
+
default=Path("/tmp/reversecore/app.log"),
|
|
86
|
+
alias="LOG_FILE",
|
|
87
|
+
description="Path to log file",
|
|
88
|
+
)
|
|
89
|
+
log_format: LogFormat = Field(
|
|
90
|
+
default=LogFormat.HUMAN,
|
|
91
|
+
alias="LOG_FORMAT",
|
|
92
|
+
description="Log format: 'human' for readable, 'json' for structured",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Error handling
|
|
96
|
+
structured_errors: bool = Field(
|
|
97
|
+
default=False,
|
|
98
|
+
description="Enable structured error responses with error codes",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Rate limiting
|
|
102
|
+
rate_limit: int = Field(
|
|
103
|
+
default=60,
|
|
104
|
+
ge=1,
|
|
105
|
+
le=1000,
|
|
106
|
+
description="Rate limit (requests per minute)",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# File size limits
|
|
110
|
+
max_output_size: int = Field(
|
|
111
|
+
default=10_000_000,
|
|
112
|
+
ge=1000,
|
|
113
|
+
description="Maximum output size for tools (bytes)",
|
|
114
|
+
)
|
|
115
|
+
lief_max_file_size: int = Field(
|
|
116
|
+
default=1_000_000_000,
|
|
117
|
+
ge=1_000_000,
|
|
118
|
+
description="Maximum file size for LIEF parsing (bytes)",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
file_retention_minutes: int = Field(
|
|
122
|
+
default=1440, # 24 hours
|
|
123
|
+
ge=60,
|
|
124
|
+
description="Retention period for temporary files (minutes)",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Transport configuration
|
|
128
|
+
mcp_transport: TransportMode = Field(
|
|
129
|
+
default=TransportMode.STDIO,
|
|
130
|
+
alias="MCP_TRANSPORT",
|
|
131
|
+
description="MCP transport mode: 'stdio' or 'http'",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Timeout configuration
|
|
135
|
+
default_tool_timeout: int = Field(
|
|
136
|
+
default=120,
|
|
137
|
+
ge=10,
|
|
138
|
+
le=3600,
|
|
139
|
+
alias="DEFAULT_TOOL_TIMEOUT",
|
|
140
|
+
description="Default timeout for tool execution (seconds)",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# R2 Pool configuration
|
|
144
|
+
r2_pool_size: int = Field(
|
|
145
|
+
default=3,
|
|
146
|
+
ge=1,
|
|
147
|
+
le=20,
|
|
148
|
+
description="Number of radare2 connections in pool",
|
|
149
|
+
)
|
|
150
|
+
r2_pool_timeout: int = Field(
|
|
151
|
+
default=30,
|
|
152
|
+
ge=5,
|
|
153
|
+
le=300,
|
|
154
|
+
description="Timeout for acquiring radare2 connection from pool",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Ghidra configuration
|
|
158
|
+
ghidra_max_projects: int = Field(
|
|
159
|
+
default=3,
|
|
160
|
+
ge=1,
|
|
161
|
+
le=10,
|
|
162
|
+
description="Maximum number of Ghidra projects to cache (higher = more RAM)",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Emulation configuration
|
|
166
|
+
max_emulation_instructions: int = Field(
|
|
167
|
+
default=1000,
|
|
168
|
+
ge=1,
|
|
169
|
+
le=1_000_000,
|
|
170
|
+
description="Maximum instructions for emulation safety limit",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# AI Memory configuration
|
|
174
|
+
memory_db_path: Path = Field(
|
|
175
|
+
default=Path.home() / ".reversecore_mcp" / "memory.db",
|
|
176
|
+
alias="MEMORY_DB_PATH",
|
|
177
|
+
description="Path to AI memory SQLite database",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@field_validator("log_level")
|
|
181
|
+
@classmethod
|
|
182
|
+
def validate_log_level(cls, v: str) -> str:
|
|
183
|
+
"""Normalize and validate log level."""
|
|
184
|
+
v = v.upper()
|
|
185
|
+
valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
|
186
|
+
if v not in valid_levels:
|
|
187
|
+
raise ValueError(f"Invalid log level: {v}. Must be one of {valid_levels}")
|
|
188
|
+
return v
|
|
189
|
+
|
|
190
|
+
@field_validator("workspace", mode="before")
|
|
191
|
+
@classmethod
|
|
192
|
+
def expand_workspace_path(cls, v: str | Path | None) -> Path:
|
|
193
|
+
"""Expand and resolve workspace path."""
|
|
194
|
+
if v is None or v == "":
|
|
195
|
+
return Path.cwd()
|
|
196
|
+
path = Path(v).expanduser().resolve()
|
|
197
|
+
return path
|
|
198
|
+
|
|
199
|
+
@field_validator("log_file", mode="before")
|
|
200
|
+
@classmethod
|
|
201
|
+
def expand_log_file_path(cls, v: str | Path) -> Path:
|
|
202
|
+
"""Expand and resolve log file path."""
|
|
203
|
+
return Path(v).expanduser().resolve()
|
|
204
|
+
|
|
205
|
+
@model_validator(mode="after")
|
|
206
|
+
def validate_workspace_exists(self) -> "Settings":
|
|
207
|
+
"""Validate workspace directory exists."""
|
|
208
|
+
if self.strict_paths:
|
|
209
|
+
if not self.workspace.exists():
|
|
210
|
+
raise ValueError(f"Workspace directory does not exist: {self.workspace}")
|
|
211
|
+
if not self.workspace.is_dir():
|
|
212
|
+
raise ValueError(f"Workspace path is not a directory: {self.workspace}")
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def read_only_dirs(self) -> tuple[Path, ...]:
|
|
217
|
+
"""Parse and return read-only directories."""
|
|
218
|
+
if not self.read_dirs:
|
|
219
|
+
return tuple()
|
|
220
|
+
parts = [s.strip() for s in self.read_dirs.split(",") if s.strip()]
|
|
221
|
+
dirs = []
|
|
222
|
+
for part in parts:
|
|
223
|
+
path = Path(part).expanduser().resolve()
|
|
224
|
+
if path.exists() and path.is_dir():
|
|
225
|
+
dirs.append(path)
|
|
226
|
+
return tuple(dirs)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# =============================================================================
|
|
230
|
+
# Legacy Config Compatibility Layer
|
|
231
|
+
# =============================================================================
|
|
232
|
+
# The following provides backward compatibility with the existing codebase
|
|
233
|
+
# that uses the Config dataclass interface.
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class Config:
|
|
237
|
+
"""Wrapper class for backward compatibility with existing code.
|
|
238
|
+
|
|
239
|
+
This class wraps the pydantic Settings model to maintain the same
|
|
240
|
+
interface as the previous dataclass-based Config.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def __init__(
|
|
244
|
+
self,
|
|
245
|
+
settings: Settings | None = None,
|
|
246
|
+
*,
|
|
247
|
+
workspace: Path | str | None = None,
|
|
248
|
+
read_only_dirs: tuple[Path, ...] | None = None,
|
|
249
|
+
log_level: str | None = None,
|
|
250
|
+
log_file: Path | str | None = None,
|
|
251
|
+
log_format: str | None = None,
|
|
252
|
+
structured_errors: bool | None = None,
|
|
253
|
+
rate_limit: int | None = None,
|
|
254
|
+
lief_max_file_size: int | None = None,
|
|
255
|
+
mcp_transport: str | None = None,
|
|
256
|
+
default_tool_timeout: int | None = None,
|
|
257
|
+
):
|
|
258
|
+
"""Initialize Config with optional Settings instance or individual values.
|
|
259
|
+
|
|
260
|
+
For backward compatibility, individual values can be passed directly.
|
|
261
|
+
"""
|
|
262
|
+
if settings is not None:
|
|
263
|
+
self._settings = settings
|
|
264
|
+
else:
|
|
265
|
+
# Build settings from individual values if provided
|
|
266
|
+
env_overrides = {}
|
|
267
|
+
if workspace is not None:
|
|
268
|
+
env_overrides["workspace"] = Path(workspace)
|
|
269
|
+
if log_level is not None:
|
|
270
|
+
env_overrides["log_level"] = log_level
|
|
271
|
+
if log_file is not None:
|
|
272
|
+
env_overrides["log_file"] = Path(log_file)
|
|
273
|
+
if log_format is not None:
|
|
274
|
+
env_overrides["log_format"] = LogFormat(log_format.lower())
|
|
275
|
+
if structured_errors is not None:
|
|
276
|
+
env_overrides["structured_errors"] = structured_errors
|
|
277
|
+
if rate_limit is not None:
|
|
278
|
+
env_overrides["rate_limit"] = rate_limit
|
|
279
|
+
if lief_max_file_size is not None:
|
|
280
|
+
env_overrides["lief_max_file_size"] = lief_max_file_size
|
|
281
|
+
if mcp_transport is not None:
|
|
282
|
+
env_overrides["mcp_transport"] = TransportMode(mcp_transport.lower())
|
|
283
|
+
if default_tool_timeout is not None:
|
|
284
|
+
env_overrides["default_tool_timeout"] = default_tool_timeout
|
|
285
|
+
|
|
286
|
+
if env_overrides:
|
|
287
|
+
self._settings = Settings(**env_overrides)
|
|
288
|
+
else:
|
|
289
|
+
self._settings = Settings()
|
|
290
|
+
|
|
291
|
+
# Store read_only_dirs if explicitly provided (for test overrides)
|
|
292
|
+
self._read_only_dirs_override = read_only_dirs
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def workspace(self) -> Path:
|
|
296
|
+
return self._settings.workspace
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def read_only_dirs(self) -> tuple[Path, ...]:
|
|
300
|
+
if self._read_only_dirs_override is not None:
|
|
301
|
+
return self._read_only_dirs_override
|
|
302
|
+
return self._settings.read_only_dirs
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def log_level(self) -> str:
|
|
306
|
+
return self._settings.log_level
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def log_file(self) -> Path:
|
|
310
|
+
return self._settings.log_file
|
|
311
|
+
|
|
312
|
+
@property
|
|
313
|
+
def log_format(self) -> str:
|
|
314
|
+
return self._settings.log_format.value
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def structured_errors(self) -> bool:
|
|
318
|
+
return self._settings.structured_errors
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def rate_limit(self) -> int:
|
|
322
|
+
return self._settings.rate_limit
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def lief_max_file_size(self) -> int:
|
|
326
|
+
return self._settings.lief_max_file_size
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def max_output_size(self) -> int:
|
|
330
|
+
return self._settings.max_output_size
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def mcp_transport(self) -> str:
|
|
334
|
+
return self._settings.mcp_transport.value
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def default_tool_timeout(self) -> int:
|
|
338
|
+
return self._settings.default_tool_timeout
|
|
339
|
+
|
|
340
|
+
@property
|
|
341
|
+
def r2_pool_size(self) -> int:
|
|
342
|
+
return self._settings.r2_pool_size
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def r2_pool_timeout(self) -> int:
|
|
346
|
+
return self._settings.r2_pool_timeout
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def ghidra_max_projects(self) -> int:
|
|
350
|
+
return self._settings.ghidra_max_projects
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def max_emulation_instructions(self) -> int:
|
|
354
|
+
return self._settings.max_emulation_instructions
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def file_retention_minutes(self) -> int:
|
|
358
|
+
return self._settings.file_retention_minutes
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def file_retention_minutes(self) -> int:
|
|
362
|
+
return self._settings.file_retention_minutes
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def from_env(cls) -> "Config":
|
|
366
|
+
"""Build a Config instance from environment variables."""
|
|
367
|
+
return cls(Settings())
|
|
368
|
+
|
|
369
|
+
def validate_paths(self, strict: bool = True) -> None:
|
|
370
|
+
"""Validate that configured directories exist and are directories."""
|
|
371
|
+
logger = logging.getLogger(__name__)
|
|
372
|
+
|
|
373
|
+
if not self.workspace.exists():
|
|
374
|
+
msg = f"Workspace directory does not exist: {self.workspace}"
|
|
375
|
+
if strict:
|
|
376
|
+
raise ValueError(msg)
|
|
377
|
+
logger.warning(msg)
|
|
378
|
+
elif not self.workspace.is_dir():
|
|
379
|
+
msg = f"Workspace path is not a directory: {self.workspace}"
|
|
380
|
+
if strict:
|
|
381
|
+
raise ValueError(msg)
|
|
382
|
+
logger.warning(msg)
|
|
383
|
+
|
|
384
|
+
for read_dir in self.read_only_dirs:
|
|
385
|
+
if not read_dir.exists():
|
|
386
|
+
msg = f"Read directory does not exist: {read_dir}"
|
|
387
|
+
if strict:
|
|
388
|
+
raise ValueError(msg)
|
|
389
|
+
logger.warning(msg)
|
|
390
|
+
elif not read_dir.is_dir():
|
|
391
|
+
msg = f"Read directory path is not a directory: {read_dir}"
|
|
392
|
+
if strict:
|
|
393
|
+
raise ValueError(msg)
|
|
394
|
+
logger.warning(msg)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# =============================================================================
|
|
398
|
+
# Module-level config management
|
|
399
|
+
# =============================================================================
|
|
400
|
+
|
|
401
|
+
_CONFIG: Config | None = None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def get_config() -> Config:
|
|
405
|
+
"""Return the cached Config instance, loading it on first access."""
|
|
406
|
+
global _CONFIG
|
|
407
|
+
if _CONFIG is None:
|
|
408
|
+
_CONFIG = Config.from_env()
|
|
409
|
+
return _CONFIG
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def get_settings() -> Settings:
|
|
413
|
+
"""Return the underlying pydantic Settings instance."""
|
|
414
|
+
return get_config()._settings
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def reset_config() -> Config:
|
|
418
|
+
"""Reload configuration from the current environment (primarily for tests)."""
|
|
419
|
+
global _CONFIG
|
|
420
|
+
_CONFIG = Config.from_env()
|
|
421
|
+
try:
|
|
422
|
+
from reversecore_mcp.core import security
|
|
423
|
+
|
|
424
|
+
security.refresh_workspace_config()
|
|
425
|
+
except Exception:
|
|
426
|
+
pass
|
|
427
|
+
return _CONFIG
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def reload_settings() -> Config:
|
|
431
|
+
"""Backward-compatible alias for legacy test helpers."""
|
|
432
|
+
return reset_config()
|