From 562eae010a2939407d7e2efe86dfa01a5824c081 Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 4 May 2026 06:19:04 +0000 Subject: [PATCH] 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 --- web/app.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/web/app.py b/web/app.py index 64a24ef..3b6779b 100644 --- a/web/app.py +++ b/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 sys.path.insert(0, str(_web_dir.parent)) 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.paperclip_client import ( archive_project as pc_archive_project, @@ -2543,6 +2550,108 @@ async def api_post_agent_comment(case_number: str, req: AgentCommentRequest): 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 ────────────────────────────── @app.get("/api/settings/paperclip-companies")