feat(scripts): כפתור "הרץ" מ-UI לסקריפטי read-only (קטגוריה B #4)
הרצת-סקריפט-מ-UI ב-/scripts, רק לסקריפטי קריאה-בלבד/אודיט, דרך גשר-המארח
הקיים (court-fetch) — שיכפול דפוס /adapter-migration.
אבטחה:
- allowlist בצד-המארח (services/script_runner.py, מקור-אמת יחיד): name→argv
קבוע ובטוח. 5 סקריפטים מאומתים: leak_guard, check_undefined_names,
storage_leak_tripwire, audit_training_corpus, audit_corpus_integrity --no-notify.
- הגשר (court_fetch_service/server.py): endpoint POST /run-script, Bearer-auth,
ולידציית-allowlist, create_subprocess_exec (ללא shell), timeout 600s,
audit-log; מתעלם מארגומנטים מהבקשה (argv מה-allowlist בלבד).
- קונטיינר (web/app.py): POX /api/scripts/{name}/run proxy ל-גשר + pre-check
allowlist; הרחבת /api/scripts/catalog ב-runnable_scripts.
- UI: כפתור "הרץ" (ירוק) רק לשורות-runnable + דיאלוג-פלט (exit-code+stdout);
שאר השורות "מקור" בלבד. confirm לפני הרצה.
מאושר דרך שער-העיצוב (מוקאפ 16-scripts עודכן עם כפתור "הרץ").
G12: leak_guard עובר (אין סמלי-פלטפורמה בשכבת-האינטליגנציה).
Deploy דו-שלבי: גשר-המארח דורש git pull בעותק-המארח + pm2 restart
legal-court-fetch-service; הקונטיינר נפרס דרך Coolify כרגיל.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
34
web/app.py
34
web/app.py
@@ -31,7 +31,7 @@ import asyncpg
|
||||
import httpx
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import chunker, db, embeddings, extractor, git_sync, metrics as metrics_service, processor, proofreader, research_md, storage
|
||||
from legal_mcp.services import chunker, db, embeddings, extractor, git_sync, metrics as metrics_service, processor, proofreader, research_md, script_runner, storage
|
||||
from legal_mcp.tools import cases as cases_tools, search as search_tools, workflow as workflow_tools, drafting as drafting_tools, precedents as precedents_tools
|
||||
from legal_mcp.tools.envelope import envelope_unwrap
|
||||
|
||||
@@ -1286,6 +1286,8 @@ async def get_scripts_catalog():
|
||||
"bytes": stat.st_size,
|
||||
"last_modified": stat.st_mtime,
|
||||
"gitea_url": gitea_url,
|
||||
# #4 — basenames the UI may offer a "הרץ" button for (read-only allowlist).
|
||||
"runnable_scripts": script_runner.runnable_names(),
|
||||
}
|
||||
|
||||
|
||||
@@ -6966,6 +6968,36 @@ async def operations_agent_migrate_adapter(req: AdapterMigrationRequest):
|
||||
return payload
|
||||
|
||||
|
||||
@app.post("/api/scripts/{name}/run")
|
||||
async def scripts_run(name: str):
|
||||
"""Run a read-only allowlisted script via the court-fetch host bridge (#4).
|
||||
|
||||
Only scripts in ``script_runner.SCRIPT_RUN_ALLOWLIST`` are runnable; the host
|
||||
bridge is the enforcer (allowlist + fixed read-only argv, no args from here).
|
||||
The script's exit code + stdout/stderr are relayed verbatim for the dashboard.
|
||||
We pre-check the allowlist here too (defense-in-depth, avoids a useless round
|
||||
trip). Audit scripts can take a while, so the timeout is generous."""
|
||||
if script_runner.build_argv(name) is None:
|
||||
raise HTTPException(403, f"סקריפט אינו ניתן-להרצה מה-UI: {name}")
|
||||
secret = os.environ.get("COURT_FETCH_SHARED_SECRET", "").strip()
|
||||
headers = {"Authorization": f"Bearer {secret}"} if secret else {}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=610.0) as client:
|
||||
r = await client.post(
|
||||
f"{_COURT_FETCH_SERVICE_URL}/run-script",
|
||||
json={"name": name}, headers=headers,
|
||||
)
|
||||
except Exception as e: # host bridge down / unreachable
|
||||
raise HTTPException(502, f"לא ניתן להגיע לשירות-המארח: {e}") from e
|
||||
try:
|
||||
payload = r.json()
|
||||
except Exception:
|
||||
payload = {"ok": False, "error": r.text[:300]}
|
||||
if r.status_code >= 400:
|
||||
raise HTTPException(r.status_code, payload.get("error", "script bridge failed"))
|
||||
return payload
|
||||
|
||||
|
||||
@app.get("/api/digests/{digest_id}")
|
||||
async def digest_get(digest_id: str):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user