"""KiloStar 统一配置管理:多 YAML 文件加载、组合校验、缓存、热重载。 配置目录:``config/``(项目根),含 config.yml、workflow.yaml、sandbox.yaml。 各文件独立维护,启动时统一加载到 ``AppConfig`` 并做 schema 校验。 """ from __future__ import annotations import os from pathlib import Path from typing import Any, List, Optional import yaml from pydantic import BaseModel, Field _CONFIG_DIR = Path(__file__).resolve().parent.parent.parent / "config" _WORKFLOW_YAML = _CONFIG_DIR / "workflow.yaml" _CONFIG_YML = _CONFIG_DIR / "config.yml" _SANDBOX_YAML = _CONFIG_DIR / "sandbox.yaml" class RetryConfig(BaseModel): max_attempts: int = Field(default=5, ge=1, le=100) class WorkflowConfig(BaseModel): retry: RetryConfig = Field(default_factory=RetryConfig) # ─── Sandbox Models (镜像 sandbox.py 中的定义,供统一加载使用) ─── class FilesystemPolicy(BaseModel): workspace_root: str = "/tmp/kilostar_workspace" allowed_read_paths: List[str] = Field(default_factory=lambda: ["/tmp"]) denied_paths: List[str] = Field(default_factory=list) class ShellPolicy(BaseModel): enabled: bool = True blocked_commands: List[str] = Field(default_factory=list) blocked_operators: List[str] = Field(default_factory=list) max_timeout: int = 60 class PythonExecutorPolicy(BaseModel): enabled: bool = True max_timeout: int = 30 blocked_imports: List[str] = Field(default_factory=list) blocked_builtins: List[str] = Field(default_factory=list) class SandboxConfig(BaseModel): enabled: bool = True filesystem: FilesystemPolicy = Field(default_factory=FilesystemPolicy) shell: ShellPolicy = Field(default_factory=ShellPolicy) python_executor: PythonExecutorPolicy = Field(default_factory=PythonExecutorPolicy) # ─── App-level Models ─── class AppInfo(BaseModel): version: str = "0.1.1-alpha" name: str = "Kilostar" class AppConfig(BaseModel): app: AppInfo = Field(default_factory=AppInfo) workflow: WorkflowConfig = Field(default_factory=WorkflowConfig) sandbox: SandboxConfig = Field(default_factory=SandboxConfig) # ─── Unified AppConfig Loading ─── _app_current: AppConfig | None = None def _read_yaml(path: Path) -> dict: if not path.exists(): return {} with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) or {} def load_app_config(config_dir: Path | None = None) -> AppConfig: """从 config/ 下多个 YAML 文件加载并组合校验。""" d = config_dir or _CONFIG_DIR app_raw = _read_yaml(d / "config.yml") workflow_raw = _read_yaml(d / "workflow.yaml") sandbox_raw = _read_yaml(d / "sandbox.yaml") sandbox_section = sandbox_raw.get("sandbox", sandbox_raw) return AppConfig.model_validate({ "app": app_raw, "workflow": workflow_raw, "sandbox": sandbox_section, }) def get_app_config() -> AppConfig: global _app_current if _app_current is None: _app_current = load_app_config() return _app_current def reload_app_config(section: str | None = None) -> AppConfig: """热重载配置。section=None 全量重载,指定 section 只刷新对应部分。""" global _app_current if section is None or _app_current is None: _app_current = load_app_config() return _app_current d = _CONFIG_DIR if section == "workflow": raw = _read_yaml(d / "workflow.yaml") _app_current = _app_current.model_copy( update={"workflow": WorkflowConfig.model_validate(raw)} ) elif section == "sandbox": raw = _read_yaml(d / "sandbox.yaml") sandbox_section = raw.get("sandbox", raw) _app_current = _app_current.model_copy( update={"sandbox": SandboxConfig.model_validate(sandbox_section)} ) else: _app_current = load_app_config() return _app_current # ─── Backward-Compatible Workflow Accessors ─── def get_workflow_config() -> WorkflowConfig: return get_app_config().workflow def reload_workflow_config() -> WorkflowConfig: reload_app_config(section="workflow") return get_app_config().workflow def save_workflow_config(config: WorkflowConfig) -> None: _WORKFLOW_YAML.parent.mkdir(parents=True, exist_ok=True) data = config.model_dump() with open(_WORKFLOW_YAML, "w", encoding="utf-8") as f: yaml.dump(data, f, default_flow_style=False, allow_unicode=True) reload_app_config(section="workflow")