CoolFace
Apppublic

devilts/sandbox-e4bc4bbc

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
sandbox_server.py356 linesDownload Raw Back to root
1"""Minimal FastAPI server for sandbox operations."""2import os, subprocess, pathlib, signal, threading, re, tempfile3from fastapi import FastAPI4from pydantic import BaseModel5from typing import Optional6import uvicorn7 8_ANSI_RE = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07')9 10def _strip_ansi(text: str) -> str:11    return _ANSI_RE.sub('', text)12 13def _truncate_output(output: str, max_chars: int = 25000, head_ratio: float = 0.25) -> str:14    if len(output) <= max_chars:15        return output16    # Write full output to temp file so LLM can read specific sections17    spill_path = None18    try:19        with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', prefix='bash_output_', dir='/tmp', delete=False) as f:20            f.write(output)21            spill_path = f.name22    except Exception:23        pass24    head_budget = int(max_chars * head_ratio)25    tail_budget = max_chars - head_budget26    head = output[:head_budget]27    tail = output[-tail_budget:]28    total = len(output)29    omitted = total - max_chars30    meta = f"\n\n... ({omitted:,} of {total:,} chars omitted, showing first {head_budget:,} + last {tail_budget:,}) ...\n"31    if spill_path:32        meta += f"Full output saved to {spill_path} — use the read tool with offset/limit to inspect specific sections.\n"33    return head + meta + tail34 35def _atomic_write(path: pathlib.Path, content: str):36    """Write atomically: temp file + fsync + os.replace."""37    path.parent.mkdir(parents=True, exist_ok=True)38    fd = None39    tmp_path = None40    try:41        fd, tmp_path = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp")42        os.write(fd, content.encode("utf-8"))43        os.fsync(fd)44        os.close(fd)45        fd = None46        os.replace(tmp_path, str(path))47        tmp_path = None48    finally:49        if fd is not None:50            os.close(fd)51        if tmp_path is not None:52            try:53                os.unlink(tmp_path)54            except OSError:55                pass56 57app = FastAPI()58 59# Track active bash processes so they can be killed on cancel60_active_procs = {}  # pid -> subprocess.Popen61_proc_lock = threading.Lock()62 63class BashReq(BaseModel):64    command: str65    work_dir: str = "/app"66    timeout: int = 12067 68class ReadReq(BaseModel):69    path: str70    offset: Optional[int] = None71    limit: Optional[int] = 200072 73class WriteReq(BaseModel):74    path: str75    content: str76 77class EditReq(BaseModel):78    path: str79    old_str: str80    new_str: str81    replace_all: bool = False82    mode: str = "replace"83 84class ExistsReq(BaseModel):85    path: str86 87# ── Fuzzy matching & edit utilities (embedded) ──88 89UNICODE_MAP = {90    "\u2013": "-", "\u2014": "-", "\u2212": "-",91    "\u2018": "'", "\u2019": "'",92    "\u201c": '"', "\u201d": '"',93    "\u00a0": " ", "\u2003": " ", "\u2002": " ",94    "\u200b": "", "\ufeff": "",95}96 97def _normalize_unicode(s):98    return "".join(UNICODE_MAP.get(c, c) for c in s)99 100def _fuzzy_find_original(content, pattern):101    """Find the original text in content that matches pattern fuzzily."""102    if pattern in content:103        return pattern, None104    # Pass 2: right-trim105    c_lines = content.split("\n")106    c_rt = "\n".join(l.rstrip() for l in c_lines)107    p_rt = "\n".join(l.rstrip() for l in pattern.split("\n"))108    if p_rt in c_rt:109        idx = c_rt.index(p_rt)110        start_line = c_rt[:idx].count("\n")111        n_lines = p_rt.count("\n") + 1112        matched = "\n".join(c_lines[start_line:start_line + n_lines])113        return matched, "(matched after trimming trailing whitespace)"114    # Pass 3: both-sides trim115    c_st = "\n".join(l.strip() for l in c_lines)116    p_st = "\n".join(l.strip() for l in pattern.split("\n"))117    if p_st in c_st:118        idx = c_st.index(p_st)119        start_line = c_st[:idx].count("\n")120        n_lines = p_st.count("\n") + 1121        matched = "\n".join(c_lines[start_line:start_line + n_lines])122        return matched, "(matched after trimming whitespace)"123    # Pass 4: unicode normalization124    c_norm = _normalize_unicode(c_st)125    p_norm = _normalize_unicode(p_st)126    if p_norm in c_norm:127        idx = c_norm.index(p_norm)128        start_line = c_norm[:idx].count("\n")129        n_lines = p_norm.count("\n") + 1130        matched = "\n".join(c_lines[start_line:start_line + n_lines])131        return matched, "(matched after unicode normalization)"132    return None, None133 134def _apply_edit(content, old_str, new_str, mode="replace", replace_all=False):135    """Apply edit. Returns (new_content, count, fuzzy_note) or raises ValueError."""136    if mode == "replace_all":137        replace_all = True138        mode = "replace"139    fuzzy_note = None140    if old_str not in content:141        matched, fuzzy_note = _fuzzy_find_original(content, old_str)142        if matched is None:143            raise ValueError("old_str not found in file.")144        old_str = matched145    count = content.count(old_str)146    if mode == "replace":147        if count > 1 and not replace_all:148            raise ValueError(f"old_str appears {count} times. Use replace_all=true or provide more context.")149        if replace_all:150            return content.replace(old_str, new_str), count, fuzzy_note151        return content.replace(old_str, new_str, 1), 1, fuzzy_note152    elif mode == "append_after":153        if replace_all:154            return content.replace(old_str, old_str + new_str), count, fuzzy_note155        idx = content.index(old_str) + len(old_str)156        return content[:idx] + new_str + content[idx:], 1, fuzzy_note157    elif mode == "prepend_before":158        if replace_all:159            return content.replace(old_str, new_str + old_str), count, fuzzy_note160        idx = content.index(old_str)161        return content[:idx] + new_str + content[idx:], 1, fuzzy_note162    raise ValueError(f"Unknown mode: {mode}")163 164def _validate_python(content, path=""):165    """Validate Python: syntax, kwargs against real installed signatures, training heuristics.166 167    Runs inside the sandbox where packages are pip-installed, so we can actually168    import classes and inspect their __init__ signatures to catch kwarg mismatches169    before runtime.170    """171    import ast as _ast, inspect as _inspect, importlib as _il172    warnings = []173 174    # 1. Syntax check175    try:176        tree = _ast.parse(content)177    except SyntaxError as e:178        warnings.append(f"Python syntax error at line {e.lineno}: {e.msg}")179        return warnings180 181    # 2. Build import map: name -> module path (from the script's own imports)182    import_map = {}183    for node in _ast.walk(tree):184        if isinstance(node, _ast.ImportFrom) and node.module:185            for alias in (node.names or []):186                local_name = alias.asname or alias.name187                import_map[local_name] = (node.module, alias.name)188        elif isinstance(node, _ast.Import):189            for alias in (node.names or []):190                local_name = alias.asname or alias.name191                import_map[local_name] = (alias.name, None)192 193    # 3. For each Call node, resolve the callable and check kwargs against signature194    for node in _ast.walk(tree):195        if not isinstance(node, _ast.Call):196            continue197        # Skip calls with **kwargs unpacking — we can't statically know those keys198        if any(kw.arg is None for kw in node.keywords):199            continue200        call_kwargs = [kw.arg for kw in node.keywords if kw.arg]201        if not call_kwargs:202            continue203 204        # Resolve the callable name205        func_name = None206        if isinstance(node.func, _ast.Name):207            func_name = node.func.id208        elif isinstance(node.func, _ast.Attribute):209            func_name = node.func.attr210        if not func_name or func_name not in import_map:211            continue212 213        # Try to import and inspect the real callable214        module_path, attr_name = import_map[func_name]215        try:216            mod = _il.import_module(module_path)217            obj = getattr(mod, attr_name, None) if attr_name else mod218            if obj is None:219                continue220            sig = _inspect.signature(obj)221            params = sig.parameters222            # If **kwargs is in the signature, any kwarg is valid223            if any(p.kind == _inspect.Parameter.VAR_KEYWORD for p in params.values()):224                continue225            valid_names = set(params.keys())226            for kw_name in call_kwargs:227                if kw_name not in valid_names:228                    warnings.append(229                        f"Invalid kwarg: {func_name}({kw_name}=...) at line {node.lineno} "230                        f"-- not accepted by {module_path}.{attr_name or func_name}()"231                    )232        except Exception:233            pass  # can't import/inspect — skip silently234 235    # 4. Training script heuristics236    if any(kw in content for kw in ("TrainingArguments", "SFTConfig", "DPOConfig", "GRPOConfig")):237        if "push_to_hub" not in content:238            warnings.append("Training script warning: no 'push_to_hub' found")239        if "hub_model_id" not in content:240            warnings.append("Training script warning: no 'hub_model_id' found")241    return warnings242 243@app.get("/api/health")244def health():245    return {"status": "ok"}246 247@app.post("/api/bash")248def bash(req: BashReq):249    try:250        proc = subprocess.Popen(251            req.command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,252            text=True, cwd=req.work_dir, start_new_session=True,253        )254        with _proc_lock:255            _active_procs[proc.pid] = proc256        try:257            stdout, stderr = proc.communicate(timeout=req.timeout)258            output = _strip_ansi(stdout + stderr)259            output = _truncate_output(output)260            return {"success": proc.returncode == 0, "output": output, "error": "" if proc.returncode == 0 else f"Exit code {proc.returncode}"}261        except subprocess.TimeoutExpired:262            try:263                os.killpg(os.getpgid(proc.pid), signal.SIGKILL)264            except OSError:265                proc.kill()266            proc.wait()267            return {"success": False, "output": "", "error": f"Timeout after {req.timeout}s"}268        finally:269            with _proc_lock:270                _active_procs.pop(proc.pid, None)271    except Exception as e:272        return {"success": False, "output": "", "error": str(e)}273 274@app.post("/api/kill")275def kill_all():276    """Kill all active bash processes. Called when user cancels."""277    with _proc_lock:278        pids = list(_active_procs.keys())279    killed = []280    for pid in pids:281        try:282            os.killpg(os.getpgid(pid), signal.SIGTERM)283            killed.append(pid)284        except OSError:285            try:286                os.kill(pid, signal.SIGKILL)287                killed.append(pid)288            except OSError:289                pass290    return {"success": True, "output": f"Killed {len(killed)} process(es): {killed}", "error": ""}291 292@app.post("/api/read")293def read(req: ReadReq):294    try:295        p = pathlib.Path(req.path)296        if not p.exists():297            return {"success": False, "output": "", "error": f"File not found: {req.path}"}298        if p.is_dir():299            return {"success": False, "output": "", "error": f"Is a directory: {req.path}"}300        lines = p.read_text().splitlines()301        start = (req.offset or 1) - 1302        end = start + (req.limit or len(lines))303        selected = lines[start:end]304        numbered = "\n".join(f"{start + i + 1}\t{line}" for i, line in enumerate(selected))305        return {"success": True, "output": numbered, "error": ""}306    except Exception as e:307        return {"success": False, "output": "", "error": str(e)}308 309@app.post("/api/write")310def write(req: WriteReq):311    try:312        p = pathlib.Path(req.path)313        _atomic_write(p, req.content)314        msg = f"Wrote {len(req.content)} bytes to {req.path}"315        if p.suffix == ".py":316            warnings = _validate_python(req.content, req.path)317            if warnings:318                msg += "\n\nValidation warnings:\n" + "\n".join(f"  ! {w}" for w in warnings)319        return {"success": True, "output": msg, "error": ""}320    except Exception as e:321        return {"success": False, "output": "", "error": str(e)}322 323@app.post("/api/edit")324def edit(req: EditReq):325    try:326        p = pathlib.Path(req.path)327        if not p.exists():328            return {"success": False, "output": "", "error": f"File not found: {req.path}"}329        content = p.read_text()330        if req.old_str == req.new_str:331            return {"success": False, "output": "", "error": "old_str and new_str must differ."}332        try:333            new_content, count, fuzzy_note = _apply_edit(334                content, req.old_str, req.new_str, mode=req.mode, replace_all=req.replace_all335            )336        except ValueError as e:337            return {"success": False, "output": "", "error": str(e)}338        _atomic_write(p, new_content)339        msg = f"Edited {req.path} ({count} replacement{'s' if count > 1 else ''})"340        if fuzzy_note:341            msg += f" {fuzzy_note}"342        if p.suffix == ".py":343            warnings = _validate_python(new_content, req.path)344            if warnings:345                msg += "\n\nValidation warnings:\n" + "\n".join(f"  ! {w}" for w in warnings)346        return {"success": True, "output": msg, "error": ""}347    except Exception as e:348        return {"success": False, "output": "", "error": str(e)}349 350@app.post("/api/exists")351def exists(req: ExistsReq):352    return {"success": True, "output": str(pathlib.Path(req.path).exists()).lower(), "error": ""}353 354if __name__ == "__main__":355    uvicorn.run(app, host="0.0.0.0", port=7860)356