feat(settings): add GET /api/settings/mcp/env endpoint
Adds four helper functions (_infisical_client, _infisical_ctx, _read_infisical_values, _build_env_var_row) and the /api/settings/mcp/env endpoint that compares Infisical vs container env vars and reports drift. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
109
web/app.py
109
web/app.py
@@ -35,6 +35,13 @@ from legal_mcp.tools import cases as cases_tools, search as search_tools, workfl
|
|||||||
_web_dir = Path(__file__).resolve().parent
|
_web_dir = Path(__file__).resolve().parent
|
||||||
sys.path.insert(0, str(_web_dir.parent))
|
sys.path.insert(0, str(_web_dir.parent))
|
||||||
from web.gitea_client import commit_and_push, create_repo, setup_remote_and_push
|
from web.gitea_client import commit_and_push, create_repo, setup_remote_and_push
|
||||||
|
from web.mcp_env_catalog import (
|
||||||
|
ENV_CATALOG,
|
||||||
|
EnvSpec,
|
||||||
|
coerce,
|
||||||
|
mask_secret,
|
||||||
|
normalize_for_compare,
|
||||||
|
)
|
||||||
from web.progress_store import ProgressStore
|
from web.progress_store import ProgressStore
|
||||||
from web.paperclip_client import (
|
from web.paperclip_client import (
|
||||||
archive_project as pc_archive_project,
|
archive_project as pc_archive_project,
|
||||||
@@ -2543,6 +2550,108 @@ async def api_post_agent_comment(case_number: str, req: AgentCommentRequest):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ── Settings: MCP Server Configuration ────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _infisical_client():
|
||||||
|
"""Build Infisical SDK client, or return None if not configured."""
|
||||||
|
token = os.environ.get("INFISICAL_TOKEN", "")
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from infisical_sdk import InfisicalSDKClient
|
||||||
|
return InfisicalSDKClient(token=token)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("infisical_client_unavailable: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _infisical_ctx():
|
||||||
|
"""Return (project_id, environment, secret_path) for legal-ai secrets."""
|
||||||
|
return (
|
||||||
|
os.environ.get("INFISICAL_PROJECT_ID", "9a77b161-f70c-4dd3-9d67-b7ab850cef51"),
|
||||||
|
os.environ.get("INFISICAL_ENV", "dev"),
|
||||||
|
os.environ.get("INFISICAL_PATH", "/legal-ai"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_infisical_values() -> tuple[dict[str, str], list[str]]:
|
||||||
|
"""Read all known catalog keys from Infisical. Returns (values, errors)."""
|
||||||
|
client = _infisical_client()
|
||||||
|
if client is None:
|
||||||
|
return {}, ["infisical_unreachable"]
|
||||||
|
project_id, env, path = _infisical_ctx()
|
||||||
|
try:
|
||||||
|
secrets = client.get_all_secrets(
|
||||||
|
environment=env, project_id=project_id, secret_path=path
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("infisical_read_failed: %s", e)
|
||||||
|
return {}, [f"infisical_read_failed: {e}"]
|
||||||
|
values: dict[str, str] = {}
|
||||||
|
for s in secrets:
|
||||||
|
if s.secret_key in ENV_CATALOG:
|
||||||
|
values[s.secret_key] = s.secret_value
|
||||||
|
return values, []
|
||||||
|
|
||||||
|
|
||||||
|
def _build_env_var_row(
|
||||||
|
spec: EnvSpec,
|
||||||
|
infisical_value: str | None,
|
||||||
|
container_value: str | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Build a single response row for an env var."""
|
||||||
|
if spec.is_secret:
|
||||||
|
i_norm = mask_secret(infisical_value) if infisical_value else None
|
||||||
|
c_norm = mask_secret(container_value) if container_value else None
|
||||||
|
# drift: compare raw before masking
|
||||||
|
drift = (
|
||||||
|
(infisical_value or "") != (container_value or "")
|
||||||
|
and bool(infisical_value or container_value)
|
||||||
|
)
|
||||||
|
infisical_display: str | None = i_norm
|
||||||
|
container_display: str | None = c_norm
|
||||||
|
else:
|
||||||
|
infisical_display = infisical_value
|
||||||
|
container_display = container_value
|
||||||
|
drift = (
|
||||||
|
normalize_for_compare(spec, infisical_value)
|
||||||
|
!= normalize_for_compare(spec, container_value)
|
||||||
|
)
|
||||||
|
# only count as drift if at least one side is non-null
|
||||||
|
if infisical_value is None and container_value is None:
|
||||||
|
drift = False
|
||||||
|
row = spec.to_public_dict()
|
||||||
|
row.update({
|
||||||
|
"infisical_value": infisical_display,
|
||||||
|
"container_value": container_display,
|
||||||
|
"drift": drift,
|
||||||
|
})
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/settings/mcp/env")
|
||||||
|
async def api_mcp_env():
|
||||||
|
"""List all catalog env vars with Infisical + container values."""
|
||||||
|
infisical_values, errors = _read_infisical_values()
|
||||||
|
project_id, env, path = _infisical_ctx()
|
||||||
|
rows = []
|
||||||
|
for key, spec in ENV_CATALOG.items():
|
||||||
|
i_val = infisical_values.get(key)
|
||||||
|
c_val = os.environ.get(key)
|
||||||
|
rows.append(_build_env_var_row(spec, i_val, c_val))
|
||||||
|
return {
|
||||||
|
"vars": rows,
|
||||||
|
"infisical_environment": env,
|
||||||
|
"infisical_project_id": project_id,
|
||||||
|
"infisical_path": path,
|
||||||
|
"coolify_app_uuid": os.environ.get(
|
||||||
|
"COOLIFY_APP_UUID", "gyjo0mtw2c42ej3xxvbz8zio"
|
||||||
|
),
|
||||||
|
"errors": errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Settings: Tag → Company Mappings ──────────────────────────────
|
# ── Settings: Tag → Company Mappings ──────────────────────────────
|
||||||
|
|
||||||
@app.get("/api/settings/paperclip-companies")
|
@app.get("/api/settings/paperclip-companies")
|
||||||
|
|||||||
Reference in New Issue
Block a user