from __future__ import annotations import asyncio from pathlib import Path from typing import Any from vibe.core.config._migration import migrate_config_layers from vibe.core.config.harness_files import ( HarnessFilesManager, get_harness_files_manager, ) from vibe.core.config.layer import ConfigLayer, RawConfig from vibe.core.config.layers.admin import AdminConfigLayer from vibe.core.config.layers.agent_profile import AgentProfileLayer from vibe.core.config.layers.default import DefaultConfigLayer from vibe.core.config.layers.environment import EnvironmentLayer from vibe.core.config.layers.growthbook import GrowthbookLayer from vibe.core.config.layers.model_catalog import ModelCatalogLayer from vibe.core.config.layers.overrides import OverridesLayer from vibe.core.config.layers.project import ProjectConfigLayer from vibe.core.config.layers.user import SuppliedUserConfigLayer, UserConfigLayer from vibe.core.config.orchestrator import ConfigOrchestrator from vibe.core.config.vibe_schema import VibeConfigSchema from vibe.utils.http import configure_ssl_context async def build_default_orchestrator( data: dict[str, Any] | None = None, *, harness_files: HarnessFilesManager | None = None, ) -> ConfigOrchestrator[VibeConfigSchema]: """Build the CLI ConfigOrchestrator with the standard layer stack. Priority order (lowest to highest): schema defaults, the model catalog, GrowthBook experiments, user TOML, project TOML, VIBE_* env vars, runtime overrides, agent profile overrides, enforced admin config. The agent-profile slot ships empty and is filled in place by AgentManager; the admin layer stays on top so an enforced org config shadows every layer below it. Both the user and project TOML layers are installed together when their sources are enabled, so a trusted project config inherits unspecified values from the user config and both TOML layers override GrowthBook assignments. The default persistence target is the user layer, otherwise a trusted project layer, otherwise an ephemeral fallback. When the harness files carry a ``user_config``, it stands in for the user TOML, and saves to it stay in memory. """ manager = harness_files or get_harness_files_manager() user_layer = _user_layer(manager) if "user" in manager.sources else None project_layer = ( ProjectConfigLayer( path=manager.cwd or Path.cwd(), trust_store=manager.trust_store ) if manager.project_source_enabled else None ) override_layer = OverridesLayer(data=data or {}) def default_layer_resolver() -> ConfigLayer[RawConfig]: # User config wins by default: a project config discovered by walking up # parents is rarely the scope the user meant in a monorepo. if user_layer is not None: return user_layer if project_layer is not None and project_layer.is_trusted: return project_layer return override_layer layers = [ DefaultConfigLayer(schema=VibeConfigSchema), # Filled at runtime from the cached or fetched model catalog. ModelCatalogLayer(), GrowthbookLayer(), *([user_layer] if user_layer is not None else []), *([project_layer] if project_layer is not None else []), EnvironmentLayer(schema=VibeConfigSchema), override_layer, # Empty slot filled in place by AgentManager when a profile is selected AgentProfileLayer(), # Highest priority: org-enforced config populated at runtime. AdminConfigLayer(), ] await migrate_config_layers(layers) orchestrator = await ConfigOrchestrator.create( schema=VibeConfigSchema, layers=layers, default_layer_resolver=default_layer_resolver, harness_files=manager, ) await asyncio.to_thread( configure_ssl_context, enable_system_trust_store=orchestrator.config.enable_system_trust_store, ) return orchestrator def _user_layer(manager: HarnessFilesManager) -> ConfigLayer[RawConfig]: if manager.user_config is None: return UserConfigLayer(path=manager.user_config_file) return SuppliedUserConfigLayer(data=manager.user_config) async def build_user_config_orchestrator() -> ConfigOrchestrator[VibeConfigSchema]: """Build a user-config orchestrator that still honors org-enforced config. Includes the admin layer so managed-config flags like ``enable_mcp`` are visible to sessionless CLI commands (``vibe mcp add`` / ``vibe mcp remove``). The layer starts empty; callers must call ``refresh_admin_layer`` to populate it from the server before relying on the enforced values. """ manager = get_harness_files_manager() user_layer = UserConfigLayer(path=manager.user_config_file) # Single-layer view: a project-defined ``local`` is invisible here, so a # sparse user entry may still get stamped. Harmless — a higher complete # entry wins the merge — and the full stack migrates with cross-file # visibility at startup. admin_layer = AdminConfigLayer() await migrate_config_layers([user_layer]) return await ConfigOrchestrator.create( schema=VibeConfigSchema, layers=[user_layer, admin_layer], default_layer_resolver=lambda: user_layer, )