sandbox_shell.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. import getpass
  2. import os
  3. import re
  4. from deepagents.backends import LocalShellBackend
  5. from deepagents.backends.protocol import ExecuteResponse
  6. from maxkb.const import CONFIG
  7. _enable_sandbox = bool(int(CONFIG.get('SANDBOX', 0)))
  8. _run_user = 'sandbox' if _enable_sandbox else getpass.getuser()
  9. _sandbox_python_sys_path = CONFIG.get_sandbox_python_package_paths().replace(',', ':')
  10. class SandboxShellBackend(LocalShellBackend):
  11. def __init__(self, root_dir: str, **kwargs):
  12. if 'env' not in kwargs and not kwargs.get('inherit_env', False):
  13. env = os.environ.copy()
  14. path = env.get('PATH', '/usr/bin:/bin')
  15. # 将 sandbox 路径分解为列表,检查每个路径是否已存在
  16. existing_paths = set(path.split(os.pathsep))
  17. sandbox_paths = _sandbox_python_sys_path.split(os.pathsep) if _sandbox_python_sys_path else []
  18. new_paths = [p for p in sandbox_paths if p and p not in existing_paths]
  19. if new_paths:
  20. env['PATH'] = f"{os.pathsep.join(new_paths)}{os.pathsep}{path}"
  21. kwargs['env'] = env
  22. super().__init__(root_dir=root_dir, **kwargs)
  23. def _translate_virtual_paths(self, command: str) -> str:
  24. """Translate virtual absolute paths in the command to real filesystem paths.
  25. In virtual_mode=True, file tools (ls, glob, read_file) return virtual absolute
  26. paths like /skills/foo.py which map to {root_dir}/skills/foo.py. But execute()
  27. runs a real shell where /skills/foo.py does not exist. This method replaces
  28. any path token that exists under root_dir with its real path, while leaving
  29. genuine system paths (e.g. /usr/bin/python3) untouched.
  30. """
  31. root = str(self.cwd)
  32. def translate(m: re.Match) -> str:
  33. virtual_path = m.group(0)
  34. real_path = root + virtual_path
  35. return real_path if os.path.lexists(real_path) else virtual_path
  36. # Match absolute-path-like tokens: / followed by a non-whitespace sequence
  37. # that isn't clearly a flag (e.g. avoid matching -/something).
  38. # Only translate when virtual_mode is active.
  39. return re.sub(r'(?<![.\w\-])/[A-Za-z_][^\s\'"\\;|&><:,]*', translate, command)
  40. def execute(
  41. self,
  42. command: str,
  43. *,
  44. timeout: int | None = None,
  45. ) -> ExecuteResponse:
  46. if self.virtual_mode:
  47. command = self._translate_virtual_paths(command)
  48. if _enable_sandbox:
  49. # 用 runuser 在子进程里切换用户,父进程凭据保持不变,
  50. # 避免父进程 ruid/euid 不一致导致 execve 报 Permission denied
  51. command = f"env -i LD_PRELOAD=/opt/maxkb-app/sandbox/lib/sandbox.so PATH=${{PATH}} gosu {_run_user} {command}"
  52. # command = f"runuser -u {_run_user} -- env -i PATH=${{PATH}} {command}"
  53. # print(f"Executing command in sandbox: {command}")
  54. return super().execute(command=command, timeout=timeout)