free-claude-code/api/admin_config/persistence.py
Ali Khokhar 51157f91bd
Refactor admin config into catalog-driven package (#926)
## Problem

Admin config was a single responsibility hub with manually duplicated
provider metadata. Provider labels, fields, template loading,
validation, persistence, and status lived in one place.

## Changes

| Before | After |
| --- | --- |
| Admin config lived in one large `api/admin_config.py` module. | Admin
config lives in package modules for manifest, sources, values,
validation, persistence, and status. |
| Provider admin fields and UI labels were manually duplicated. |
Provider admin fields and display names derive from `PROVIDER_CATALOG`
with admin-only help overrides. |
| `fcc-init` and Admin UI loaded `.env.example` separately. | `fcc-init`
and Admin UI use shared `config.env_template` loading. |
| Architecture docs pointed to the old admin config module. |
Architecture docs describe the package owners and catalog-driven
provider manifest. |

<!-- greptile_comment -->

<details open><summary><h3>Greptile Summary</h3></summary>

This PR refactors admin configuration into a catalog-driven package. The
main changes are:

- Split the former monolithic `api/admin_config.py` into manifest,
source loading, value presentation, validation, persistence, and
provider status modules.
- Generate provider admin fields and display names from
`PROVIDER_CATALOG` with admin-specific help overrides.
- Share `.env.example` loading between `fcc-init` and Admin UI defaults
through `config.env_template`.
- Update admin routes, Admin UI provider labels, architecture docs,
version metadata, and contract/API tests for the new module layout.
</details>

<h3>Confidence Score: 5/5</h3>

The refactor appears merge-safe with no code issues identified in the
reviewed changes.

The package split, catalog-driven provider metadata, shared environment
template loading, route updates, and tests/docs changes are cohesive and
covered by corresponding contract/API/CLI test updates.

<details><summary><h3><a href="https://www.greptile.com/trex"><img
alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="20" align="absmiddle"></a> T-Rex Logs</h3></summary>

**What T-Rex did**
- T-Rex ran manifest validation for catalog provider before and after
routes, capturing base and head responses and catalog-alignment checks,
and confirmed the validation completed successfully.
- T-Rex evaluated the shared-env-template scenarios, observing the
before run with no config.env\_template module and the after run with
the module present, with patched loader values and all consistency
checks passing, and the run exited with code 0.
- T-Rex executed the package-admin-workflow validation, verifying the
base and after import paths, the load/validate/write workflow produced
matching outputs, and the run completed with exit code 0.

<a
href="https://app.greptile.com/trex/runs/12529845/artifacts"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=1"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1"><img
alt="View all artifacts"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=1"
height="32"></picture></a>

<sub><a href="https://www.greptile.com/trex"><img alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="14" align="absmiddle"></a> Ran code and verified through
T-Rex</sub>
</details>

<sub>Reviews (1): Last reviewed commit: ["Refactor admin config into
catalog-drive..."](d6239d7953)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=40315222)</sub>

<!-- /greptile_comment -->
2026-06-27 15:37:29 -07:00

149 lines
5 KiB
Python

"""Managed env persistence, validation preview, and rendering."""
from __future__ import annotations
import os
from collections.abc import Mapping
from typing import Any
from config.paths import managed_env_path
from .manifest import FIELD_BY_KEY, FIELDS, SECTIONS, ConfigFieldSpec
from .sources import dotenv_values_from_file, is_locked_source, template_values
from .validation import validate_values
from .values import MASKED_SECRET, load_value_state, normalize_for_env
def target_values_with_updates(updates: Mapping[str, Any]) -> dict[str, str]:
"""Return managed env values after applying admin updates."""
state = load_value_state()
values = template_values()
# Preserve existing managed values when present. If no managed config exists,
# seed the first write from effective repo values to migrate legacy setups.
managed_values = dotenv_values_from_file(managed_env_path())
if managed_values:
values.update(
{key: val for key, val in managed_values.items() if key in values}
)
else:
for key, entry in state.items():
if entry["source"] in {"repo_env", "template", "default"}:
values[key] = str(entry["value"])
for key, value in updates.items():
field = FIELD_BY_KEY.get(key)
if field is None:
continue
if is_locked_source(state[key]["source"]):
continue
if field.secret and value == MASKED_SECRET:
continue
values[key] = normalize_for_env(value)
for field in FIELDS:
values.setdefault(field.key, field.default)
return values
def effective_values_for_validation(
target_values: Mapping[str, str],
) -> dict[str, str]:
"""Return values validated after preserving locked external sources."""
values = dict(target_values)
for key, entry in load_value_state().items():
if is_locked_source(entry["source"]):
values[key] = str(entry["value"])
return values
def validate_updates(updates: Mapping[str, Any]) -> dict[str, Any]:
"""Validate partial admin updates and return a masked generated env preview."""
target_values = target_values_with_updates(updates)
effective_values = effective_values_for_validation(target_values)
valid, errors = validate_values(effective_values)
return {
"valid": valid,
"errors": errors,
"env_preview": render_env_file(target_values, mask_secrets=True),
}
def changed_pending_fields(updates: Mapping[str, Any]) -> list[str]:
"""Return changed fields that require manual runtime action."""
state = load_value_state()
pending: list[str] = []
for key, value in updates.items():
field = FIELD_BY_KEY.get(key)
if field is None or not (field.restart_required or field.session_sensitive):
continue
if normalize_for_env(value) == str(state[key]["value"]):
continue
pending.append(key)
return pending
def write_managed_env(updates: Mapping[str, Any]) -> dict[str, Any]:
"""Validate and atomically write the admin-managed env file."""
validation = validate_updates(updates)
if not validation["valid"]:
return validation | {"applied": False, "pending_fields": []}
target_values = target_values_with_updates(updates)
pending_fields = changed_pending_fields(updates)
path = managed_env_path()
path.parent.mkdir(parents=True, exist_ok=True)
temp_path = path.with_suffix(path.suffix + ".tmp")
temp_path.write_text(render_env_file(target_values), encoding="utf-8")
os.replace(temp_path, path)
return {
"applied": True,
"valid": True,
"errors": [],
"env_preview": render_env_file(target_values, mask_secrets=True),
"path": str(path),
"pending_fields": pending_fields,
}
def quote_env_value(value: str) -> str:
"""Quote a value when dotenv syntax requires it."""
if value == "":
return ""
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
if any(char.isspace() for char in value) or any(
char in value for char in ('"', "#", "=", "$")
):
return f'"{escaped}"'
return value
def render_env_file(values: Mapping[str, str], *, mask_secrets: bool = False) -> str:
"""Render a complete grouped env file."""
lines: list[str] = [
"# Managed by Free Claude Code /admin.",
"# Edit in the server UI when possible.",
"",
]
fields_by_section: dict[str, list[ConfigFieldSpec]] = {
section.section_id: [] for section in SECTIONS
}
for field in FIELDS:
fields_by_section.setdefault(field.section_id, []).append(field)
for section in SECTIONS:
lines.append(f"# {section.label}")
for field in fields_by_section.get(section.section_id, []):
value = values.get(field.key, field.default)
if mask_secrets and field.secret and value:
value = MASKED_SECRET
lines.append(f"{field.key}={quote_env_value(value)}")
lines.append("")
return "\n".join(lines).rstrip() + "\n"