fix(settings): harden PATCH/redeploy per code review

- Add infisicalsdk dependency
- Narrow update→create fallback to NotFound errors only (no silent swallow)
- Truncate Coolify error response text to 200 chars
- Add 60s cooldown to redeploy endpoint
- Move httpx to top-level import
This commit is contained in:
2026-05-04 06:33:01 +00:00
parent 2fe73fcce1
commit 69bdf7b30a
2 changed files with 41 additions and 7 deletions

View File

@@ -20,6 +20,7 @@ dependencies = [
"fastapi>=0.115.0", "fastapi>=0.115.0",
"uvicorn[standard]>=0.30.0", "uvicorn[standard]>=0.30.0",
"httpx>=0.27.0", "httpx>=0.27.0",
"infisicalsdk>=1.0.0",
] ]
[build-system] [build-system]

View File

@@ -26,6 +26,7 @@ from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
import asyncpg import asyncpg
import httpx
from legal_mcp import config from legal_mcp import config
from legal_mcp.services import chunker, db, embeddings, extractor, git_sync, processor, proofreader, research_md from legal_mcp.services import chunker, db, embeddings, extractor, git_sync, processor, proofreader, research_md
@@ -2552,6 +2553,12 @@ async def api_post_agent_comment(case_number: str, req: AgentCommentRequest):
# ── Settings: MCP Server Configuration ──────────────────────────── # ── Settings: MCP Server Configuration ────────────────────────────
# Module-level guard: minimum interval between redeploys (60 seconds).
# Prevents accidental double-clicks or automated retry loops from queueing
# multiple redundant Coolify builds.
_LAST_REDEPLOY_AT: float = 0.0
_REDEPLOY_MIN_INTERVAL_SEC: float = 60.0
def _infisical_client(): def _infisical_client():
"""Build Infisical SDK client, or return None if not configured.""" """Build Infisical SDK client, or return None if not configured."""
@@ -2699,10 +2706,12 @@ async def api_mcp_env_update(key: str, req: McpEnvUpdateRequest):
"false" if coerced is False else str(coerced) "false" if coerced is False else str(coerced)
) )
try: try:
# SDK pattern: try update, fall back to create if missing. # SDK pattern: try update, fall back to create if the secret doesn't exist.
# NOTE: exact method may vary by infisical-python version. The # The Infisical SDK's specific NotFound exception class isn't stable across
# canonical method is `update_secret_by_name`; if your version # versions, so we inspect the error string. Network/auth errors (which DON'T
# uses `secrets.update`, replace accordingly. # contain 'not found' / 404 / 'does not exist') are re-raised immediately
# so they reach the outer handler and surface as 502 with the real error,
# rather than being silently retried as a create-call.
try: try:
client.update_secret_by_name( client.update_secret_by_name(
project_id=project_id, project_id=project_id,
@@ -2711,7 +2720,22 @@ async def api_mcp_env_update(key: str, req: McpEnvUpdateRequest):
secret_name=key, secret_name=key,
secret_value=str_value, secret_value=str_value,
) )
except Exception: except Exception as update_err:
err_text = str(update_err).lower()
looks_like_missing = (
"not found" in err_text
or "does not exist" in err_text
or "404" in err_text
)
if not looks_like_missing:
logger.warning(
"infisical_update_failed key=%s err=%s — not retrying as create",
key, update_err,
)
raise
logger.info(
"infisical_secret_missing key=%s — falling back to create", key,
)
client.create_secret_by_name( client.create_secret_by_name(
project_id=project_id, project_id=project_id,
environment_slug=env, environment_slug=env,
@@ -2740,7 +2764,14 @@ async def api_mcp_env_update(key: str, req: McpEnvUpdateRequest):
@app.post("/api/settings/mcp/env/redeploy") @app.post("/api/settings/mcp/env/redeploy")
async def api_mcp_env_redeploy(): async def api_mcp_env_redeploy():
"""Trigger Coolify redeploy of the legal-ai app.""" """Trigger Coolify redeploy of the legal-ai app."""
import httpx global _LAST_REDEPLOY_AT
now = time.time()
elapsed = now - _LAST_REDEPLOY_AT
if elapsed < _REDEPLOY_MIN_INTERVAL_SEC:
wait = int(_REDEPLOY_MIN_INTERVAL_SEC - elapsed)
raise HTTPException(
429, f"Redeploy בהמתנה: נסה שוב בעוד {wait} שניות."
)
coolify_url = os.environ.get("COOLIFY_URL", "http://158.178.131.193:8000") coolify_url = os.environ.get("COOLIFY_URL", "http://158.178.131.193:8000")
coolify_token = os.environ.get("COOLIFY_API_TOKEN", "") coolify_token = os.environ.get("COOLIFY_API_TOKEN", "")
app_uuid = os.environ.get("COOLIFY_APP_UUID", "gyjo0mtw2c42ej3xxvbz8zio") app_uuid = os.environ.get("COOLIFY_APP_UUID", "gyjo0mtw2c42ej3xxvbz8zio")
@@ -2757,8 +2788,9 @@ async def api_mcp_env_redeploy():
except Exception as e: except Exception as e:
raise HTTPException(502, f"Coolify unreachable: {e}") raise HTTPException(502, f"Coolify unreachable: {e}")
if resp.status_code >= 400: if resp.status_code >= 400:
body_preview = (resp.text or "")[:200]
raise HTTPException( raise HTTPException(
502, f"Coolify deploy failed: {resp.status_code} {resp.text}" 502, f"Coolify deploy failed: {resp.status_code} {body_preview}"
) )
data = resp.json() if resp.content else {} data = resp.json() if resp.content else {}
deployment_uuid = ( deployment_uuid = (
@@ -2766,6 +2798,7 @@ async def api_mcp_env_redeploy():
or (data.get("deployments") or [{}])[0].get("deployment_uuid") or (data.get("deployments") or [{}])[0].get("deployment_uuid")
) )
logger.info("mcp_env_redeploy triggered uuid=%s", deployment_uuid) logger.info("mcp_env_redeploy triggered uuid=%s", deployment_uuid)
_LAST_REDEPLOY_AT = now
return { return {
"ok": True, "ok": True,
"deployment_uuid": deployment_uuid, "deployment_uuid": deployment_uuid,