qualifier / backend /app /qualifier_script_runner.py
Sethu0330's picture
Add FTL RFQ Qualifier agent + full SALES CRM app for HF Spaces testing
005df22 unverified
Raw History Blame Contribute Delete
3.41 kB
"""
Admin-only sandboxed execution for Qualifier skill scripts.
Near-identical to chat_agent_script_runner.py (same isolation boundary: fixed minimal env
allowlist, wall-clock timeout, no OS-level sandbox beyond that). Kept as its own module rather than
imported, for the same "separate copy per agent" reasoning as the rest of the qualifier_*.py files
— this is reachable only from POST /api/qualifier/skills/{id}/scripts/{index}/run, guarded by
require_tenant_admin, and never by the background qualification run itself.
"""
from __future__ import annotations
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, Dict
MAX_OUTPUT_CHARS = 20_000
DEFAULT_TIMEOUT_SECONDS = 10
_SUPPORTED_LANGUAGES = {"python", "py", ""}
def run_script_sandboxed(script: Dict[str, Any], timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS) -> Dict[str, Any]:
"""Run one skill script in an isolated subprocess and return its result."""
language = (script.get("language") or "").strip().lower()
content = script.get("content") or ""
if language not in _SUPPORTED_LANGUAGES:
return {
"status": "unsupported",
"detail": f'Only Python scripts can be run in this environment (got "{language}").',
"stdout": "",
"stderr": "",
"duration_ms": 0,
}
if not content.strip():
return {"status": "error", "detail": "Script has no content.", "stdout": "", "stderr": "", "duration_ms": 0}
tmpdir = tempfile.mkdtemp(prefix="qualifier_script_")
started = time.monotonic()
try:
script_path = Path(tmpdir) / "script.py"
script_path.write_text(content)
minimal_env = {
"PATH": "/usr/bin:/bin:/usr/local/bin",
"LANG": "C.UTF-8",
"HOME": tmpdir,
"PYTHONDONTWRITEBYTECODE": "1",
}
proc = subprocess.run(
[sys.executable, str(script_path)],
cwd=tmpdir,
env=minimal_env,
timeout=timeout_seconds,
capture_output=True,
text=True,
)
duration_ms = int((time.monotonic() - started) * 1000)
return {
"status": "ok" if proc.returncode == 0 else "error",
"detail": "" if proc.returncode == 0 else f"Exited with code {proc.returncode}",
"stdout": proc.stdout[:MAX_OUTPUT_CHARS],
"stderr": proc.stderr[:MAX_OUTPUT_CHARS],
"duration_ms": duration_ms,
}
except subprocess.TimeoutExpired as e:
duration_ms = int((time.monotonic() - started) * 1000)
stdout = e.stdout if isinstance(e.stdout, str) else (e.stdout or b"").decode("utf-8", "replace")
stderr = e.stderr if isinstance(e.stderr, str) else (e.stderr or b"").decode("utf-8", "replace")
return {
"status": "timeout",
"detail": f"Script did not finish within {timeout_seconds}s and was killed.",
"stdout": stdout[:MAX_OUTPUT_CHARS],
"stderr": stderr[:MAX_OUTPUT_CHARS],
"duration_ms": duration_ms,
}
except Exception as e:
duration_ms = int((time.monotonic() - started) * 1000)
return {"status": "error", "detail": str(e)[:500], "stdout": "", "stderr": "", "duration_ms": duration_ms}
finally:
shutil.rmtree(tmpdir, ignore_errors=True)