feat(ops): פאנל "מתאמי-סוכנים" ב-/operations — מעבר-אדפטר בכפתור (any→any)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
שלב-ה-UI של מנגנון מעבר-האדפטר (PR #247). הכפתור ב-/operations מריץ את scripts/migrate_agent_adapter.py בהוסט דרך גשר-court-fetch (הקונטיינר לא יכול לבצע — צריך FS-הוסט + DB-המובנה), בדיוק כמו כפתורי-pm2. - court_fetch_service/server.py: endpoint /adapter-migration מאומת (Bearer, COURT_FETCH_SHARED_SECRET) שמריץ את הסקריפט עם allowlist-פעולות ו-args אטומיים (exec, ללא shell; הסקריפט מאמת). symbol-light לשמירת G12 בשכבת mcp-server. - web/app.py: proxy POST /api/operations/agents/migrate-adapter → הגשר. - web-ui: useMigrateAdapter (operations.ts) + AgentAdaptersPanel לפי המוקאפ המאושר 02d-operations-adapters.html: roster per-role (מתאם נוכחי+מודל+בורר-יעד), סרגל-חירום גלובלי (הכל→Gemini/החזר→Claude), preflight בדיאלוג + toggle שחרור-כלים, דגלי מועבר/א-סימטרי. מחזר usePaperclipAgents לתצוגה. עיצוב אושר ע"י חיים בשער Claude Design (פרויקט IA Redesign X17). נבדק: tsc --noEmit נקי, eslint נקי. Invariants: G12 (גשר symbol-light; הסקריפט בשכבת scripts/ הוא שמדבר Paperclip), INV-MC1 (שתי החברות יחד), INV-IA "מקום אחד" (/operations). המשך FU-8a. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -288,6 +288,76 @@ async def fetch(request: web.Request) -> web.Response:
|
||||
return web.json_response({"ok": False, "reason": f"unexpected: {e}"}, status=200)
|
||||
|
||||
|
||||
# ─── adapter-migration: host-side runner for scripts/migrate_agent_adapter.py ───
|
||||
# The legal-ai container can't perform the migration itself (it needs the host
|
||||
# filesystem — generated instruction copies, the gemini settings file — plus the
|
||||
# embedded board DB), so the dashboard proxies the action here. Mutating, so it
|
||||
# requires the Bearer secret like /pm2/control. We launch exactly one fixed,
|
||||
# in-repo script with create_subprocess_exec (no shell) and an action allowlist;
|
||||
# every other argument is passed through opaque and validated by the script
|
||||
# itself. Kept deliberately symbol-light so this host bridge stays generic.
|
||||
_MIGRATE_SCRIPT = "/home/chaim/legal-ai/scripts/migrate_agent_adapter.py"
|
||||
_MIGRATE_PYTHON = "/home/chaim/legal-ai/mcp-server/.venv/bin/python"
|
||||
_MIGRATE_ACTIONS = {"check", "apply", "revert", "verify"}
|
||||
|
||||
|
||||
async def adapter_migration(request: web.Request) -> web.Response:
|
||||
"""Run scripts/migrate_agent_adapter.py on the host and relay its result."""
|
||||
unauth = _check_bearer(request)
|
||||
if unauth is not None:
|
||||
return unauth
|
||||
try:
|
||||
body = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
return web.json_response({"error": "invalid JSON body"}, status=400)
|
||||
|
||||
action = str(body.get("action", "")).strip()
|
||||
if action not in _MIGRATE_ACTIONS:
|
||||
return web.json_response(
|
||||
{"error": f"action must be one of {sorted(_MIGRATE_ACTIONS)}"}, status=400
|
||||
)
|
||||
|
||||
argv = [_MIGRATE_PYTHON, _MIGRATE_SCRIPT, f"--{action}"]
|
||||
agent = str(body.get("agent", "")).strip()
|
||||
target = str(body.get("to", "")).strip()
|
||||
model = str(body.get("model", "")).strip()
|
||||
if action in ("check", "apply", "revert"):
|
||||
if not agent:
|
||||
return web.json_response({"error": "agent required"}, status=400)
|
||||
argv += ["--agent", agent]
|
||||
if action in ("check", "apply"):
|
||||
if not target:
|
||||
return web.json_response({"error": "to (target) required"}, status=400)
|
||||
argv += ["--to", target]
|
||||
if model:
|
||||
argv += ["--model", model]
|
||||
if bool(body.get("relax_tools")):
|
||||
argv += ["--relax-tools"]
|
||||
|
||||
import asyncio as _asyncio
|
||||
|
||||
env = {**os.environ, "HOME": "/home/chaim"}
|
||||
try:
|
||||
proc = await _asyncio.create_subprocess_exec(
|
||||
*argv, cwd="/home/chaim/legal-ai", env=env,
|
||||
stdout=_asyncio.subprocess.PIPE, stderr=_asyncio.subprocess.PIPE,
|
||||
)
|
||||
out, err = await _asyncio.wait_for(proc.communicate(), timeout=180)
|
||||
except _asyncio.TimeoutError:
|
||||
return web.json_response({"ok": False, "error": "migration timed out"}, status=504)
|
||||
except Exception as e: # never throw — relay the failure
|
||||
return web.json_response({"ok": False, "error": f"launch failed: {e}"}, status=502)
|
||||
|
||||
# 200 regardless of exit code: a non-zero --check (preflight refusal) is an
|
||||
# informative result the caller renders, not a transport error.
|
||||
return web.json_response({
|
||||
"ok": (proc.returncode == 0),
|
||||
"exit_code": proc.returncode,
|
||||
"stdout": out.decode("utf-8", "replace"),
|
||||
"stderr": err.decode("utf-8", "replace"),
|
||||
})
|
||||
|
||||
|
||||
def build_app() -> web.Application:
|
||||
app = web.Application(client_max_size=64 * 1024 * 1024)
|
||||
app.router.add_get("/health", health)
|
||||
@@ -295,6 +365,7 @@ def build_app() -> web.Application:
|
||||
app.router.add_get("/usage", usage_status)
|
||||
app.router.add_post("/pm2/control", pm2_control)
|
||||
app.router.add_post("/fetch", fetch)
|
||||
app.router.add_post("/adapter-migration", adapter_migration)
|
||||
return app
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user