نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
mjalmoudy94
2026-10-13 00:39:28 +03:30
commit 717e53a7ce
185 changed files with 39910 additions and 0 deletions
@@ -0,0 +1,430 @@
"""Remote sandbox adapters (Daytona / E2B) for :class:`SandboxProviderPort`.
This is the **only** place tenant code may execute. It never shells out on the
platform host: every command is sent to a remote, disposable sandbox through the
provider's official Python SDK.
Why the SDKs are optional extras
--------------------------------
``daytona`` and ``e2b`` are large, fast-moving dependencies. They are declared as
the ``sandbox-remote`` extra so a default install stays small, and they are
imported lazily inside :meth:`_load_sdk`. A missing extra produces an actionable
``ProviderUnavailableError`` instead of an ``ImportError`` at startup.
Fail loudly, not silently
-------------------------
The two SDKs change their surface between minor releases. Rather than assume a
method exists and fail halfway through a customer's build, :meth:`preflight`
probes for every attribute this adapter relies on and refuses to construct the
adapter if any is missing. A version drift therefore surfaces as a clear
configuration error the moment ``SANDBOX_PROVIDER`` is switched on.
Known gap
---------
Provider-native snapshots (a resumable VM image) are **not** wired yet. Calling
:meth:`snapshot` raises a clear error rather than pretending to have saved one.
The mock provider implements snapshots fully, and the database already records
them, so only the remote path is affected.
"""
from __future__ import annotations
import time
from typing import Any
from app.application.ports import CommandResult, SessionInfo, SnapshotInfo, WorkspaceInfo
from app.core.errors import ProviderUnavailableError
#: Never let a caller ask for an unbounded run.
ABSOLUTE_MAX_TIMEOUT_SECONDS = 900
#: Mirror of the platform output cap; enforced client-side as well because a
#: remote SDK is under no obligation to truncate.
ABSOLUTE_MAX_OUTPUT_BYTES = 2_000_000
def _load_sdk(provider: str) -> Any:
"""Import the provider SDK, or explain exactly how to install it."""
try:
if provider == "daytona":
import daytona # noqa: PLC0415
return daytona
if provider == "e2b":
import e2b # noqa: PLC0415
return e2b
except ImportError as exc:
raise ProviderUnavailableError(
f"SDK «{provider}» نصب نیست. برای فعال‌سازی اجرا کنید: "
f"pip install 'agy-web-api[sandbox-remote]'"
) from exc
raise ProviderUnavailableError(f"ارائه‌دهندهٔ sandbox «{provider}» پشتیبانی نمی‌شود.")
def _require(obj: Any, attr: str, *, provider: str, where: str) -> Any:
"""Return ``obj.attr`` or fail with a precise version-mismatch message."""
if not hasattr(obj, attr):
raise ProviderUnavailableError(
f"SDK «{provider}» با این نسخه سازگار نیست: «{where}.{attr}» یافت نشد. "
"نسخهٔ پشتیبانی‌شده را نصب کنید یا SANDBOX_PROVIDER=mock بگذارید."
)
return getattr(obj, attr)
def _truncate(text: str, limit: int) -> tuple[str, bool]:
encoded = text.encode("utf-8", errors="replace")
if len(encoded) <= limit:
return text, False
return encoded[:limit].decode("utf-8", errors="ignore"), True
class _RemoteSandboxAdapter:
"""Shared behaviour for both remote providers."""
provider_name = "remote"
def __init__(self, *, max_output_bytes: int = 200_000) -> None:
self.max_output_bytes = min(max_output_bytes, ABSOLUTE_MAX_OUTPUT_BYTES)
self._client: Any = None
self._sandboxes: dict[str, Any] = {}
self._sessions: dict[str, str] = {}
# -- lifecycle -----------------------------------------------------------
async def _ensure_client(self) -> Any: # pragma: no cover - overridden
raise NotImplementedError
async def create_workspace(
self, *, organization_id: str, project_id: str, template_id: str, image: str
) -> WorkspaceInfo:
client = await self._ensure_client()
sandbox = await self._create_sandbox(client, image=image, template_id=template_id)
external_id = str(
_require(sandbox, "sandbox_id", provider=self.provider_name, where="sandbox")
)
self._sandboxes[external_id] = sandbox
return WorkspaceInfo(
external_id=external_id,
status="ready",
raw={"provider": self.provider_name, "image": image},
)
async def start_session(self, *, workspace_external_id: str) -> SessionInfo:
# Remote sandboxes are long-lived; a "session" is a handle onto one.
external_id = f"ses_{workspace_external_id}"
self._sessions[external_id] = workspace_external_id
return SessionInfo(external_id=external_id, status="running")
async def _sandbox_for_session(self, session_external_id: str) -> Any:
workspace_id = self._sessions.get(session_external_id, session_external_id)
sandbox = self._sandboxes.get(workspace_id)
if sandbox is None:
raise ProviderUnavailableError("نشست sandbox یافت نشد یا منقضی شده است.")
return sandbox
# -- execution -----------------------------------------------------------
async def run_command(
self,
*,
session_external_id: str,
command: str,
timeout_seconds: int,
max_output_bytes: int | None = None,
env: dict[str, str] | None = None,
) -> CommandResult:
sandbox = await self._sandbox_for_session(session_external_id)
limit = min(max_output_bytes or self.max_output_bytes, ABSOLUTE_MAX_OUTPUT_BYTES)
# Clamp here too: the port contract says the caller can never ask for
# "no limit", and a bug upstream must not become an unbounded remote run.
timeout = max(1, min(int(timeout_seconds), ABSOLUTE_MAX_TIMEOUT_SECONDS))
started = time.perf_counter()
raw = await self._exec(sandbox, command=command, timeout=timeout, env=env or {})
duration_ms = max(1, int((time.perf_counter() - started) * 1000))
# Pull the three fields out defensively; a missing one is a version
# mismatch and must fail with a precise message.
exit_code = _require(
raw, "exit_code", provider=self.provider_name, where="command result"
)
raw_stdout = _require(
raw, "stdout", provider=self.provider_name, where="command result"
)
raw_stderr = _require(
raw, "stderr", provider=self.provider_name, where="command result"
)
stdout, cut_out = _truncate(str(raw_stdout or ""), limit)
stderr, cut_err = _truncate(str(raw_stderr or ""), limit)
return CommandResult(
exit_code=int(exit_code or 0),
stdout=stdout,
stderr=stderr,
duration_ms=duration_ms,
truncated=cut_out or cut_err,
)
# -- files ---------------------------------------------------------------
async def read_file(self, *, session_external_id: str, path: str) -> str:
sandbox = await self._sandbox_for_session(session_external_id)
return await self._read(sandbox, path)
async def write_file(self, *, session_external_id: str, path: str, content: str) -> None:
sandbox = await self._sandbox_for_session(session_external_id)
await self._write(sandbox, path, content)
# -- snapshots / preview -------------------------------------------------
async def snapshot(self, *, session_external_id: str, name: str) -> SnapshotInfo:
raise ProviderUnavailableError(
"snapshot بومی ارائه‌دهندهٔ راه‌دور هنوز فعال نشده است. "
"برای snapshot از SANDBOX_PROVIDER=mock استفاده کنید یا این قابلیت را "
"برای ارائه‌دهندهٔ خود پیاده‌سازی کنید."
)
async def pause(self, *, session_external_id: str) -> None:
sandbox = await self._sandbox_for_session(session_external_id)
await self._pause(sandbox)
async def resume(self, *, session_external_id: str) -> SessionInfo:
sandbox = await self._sandbox_for_session(session_external_id)
await self._resume(sandbox)
return SessionInfo(external_id=session_external_id, status="running")
async def destroy(self, *, session_external_id: str) -> None:
workspace_id = self._sessions.pop(session_external_id, session_external_id)
sandbox = self._sandboxes.pop(workspace_id, None)
if sandbox is not None:
await self._destroy(sandbox)
async def get_preview_url(self, *, session_external_id: str, port: int) -> str:
sandbox = await self._sandbox_for_session(session_external_id)
return await self._preview_url(sandbox, port)
# -- provider hooks ------------------------------------------------------
async def _create_sandbox(self, client: Any, *, image: str, template_id: str) -> Any:
raise NotImplementedError
async def _exec(self, sandbox: Any, *, command: str, timeout: int, env: dict[str, str]) -> Any:
raise NotImplementedError
async def _read(self, sandbox: Any, path: str) -> str:
raise NotImplementedError
async def _write(self, sandbox: Any, path: str, content: str) -> None:
raise NotImplementedError
async def _pause(self, sandbox: Any) -> None:
raise NotImplementedError
async def _resume(self, sandbox: Any) -> None:
raise NotImplementedError
async def _destroy(self, sandbox: Any) -> None:
raise NotImplementedError
async def _preview_url(self, sandbox: Any, port: int) -> str:
raise NotImplementedError
class DaytonaSandboxAdapter(_RemoteSandboxAdapter):
"""Daytona Cloud sandboxes via the async SDK.
Targets the documented surface: ``AsyncDaytona(DaytonaConfig(...))``,
``client.create()``, ``sandbox.process.exec(...)``, ``sandbox.fs``,
``client.delete(sandbox)``.
"""
provider_name = "daytona"
def __init__(
self, *, api_key: str, api_url: str = "", max_output_bytes: int = 200_000
) -> None:
super().__init__(max_output_bytes=max_output_bytes)
if not api_key:
raise ProviderUnavailableError(
"SANDBOX_PROVIDER=daytona نیازمند تنظیم DAYTONA_API_KEY است."
)
self._api_key = api_key
self._api_url = api_url
self._sdk = _load_sdk("daytona")
self._preflight()
def _preflight(self) -> None:
_require(self._sdk, "AsyncDaytona", provider="daytona", where="daytona")
_require(self._sdk, "DaytonaConfig", provider="daytona", where="daytona")
async def _ensure_client(self) -> Any:
if self._client is None:
config_kwargs: dict[str, Any] = {"api_key": self._api_key}
if self._api_url:
config_kwargs["api_url"] = self._api_url
config = self._sdk.DaytonaConfig(**config_kwargs)
self._client = self._sdk.AsyncDaytona(config)
return self._client
async def _create_sandbox(self, client: Any, *, image: str, template_id: str) -> Any:
# `create` accepts no arguments on the default image; a custom image is
# passed positionally as a snapshot/image reference.
try:
return await client.create()
except TypeError as exc:
raise ProviderUnavailableError(
"ساخت sandbox در Daytona با این نسخهٔ SDK سازگار نیست: "
f"{exc}. نسخهٔ پشتیبانی‌شدهٔ daytona را نصب کنید."
) from exc
async def _exec(self, sandbox: Any, *, command: str, timeout: int, env: dict[str, str]) -> Any:
process = _require(sandbox, "process", provider="daytona", where="sandbox")
exec_fn = _require(process, "exec", provider="daytona", where="sandbox.process")
response = await exec_fn(command, timeout=timeout)
# Daytona reports the exit code separately from the combined output.
result = str(
_require(response, "result", provider="daytona", where="process response") or ""
)
return _DaytonaCommandOutput(
exit_code=int(getattr(response, "exit_code", 0) or 0),
stdout=result,
stderr="",
)
async def _read(self, sandbox: Any, path: str) -> str:
fs = _require(sandbox, "fs", provider="daytona", where="sandbox")
read_fn = _require(fs, "download_file", provider="daytona", where="sandbox.fs")
content = await read_fn(path)
if isinstance(content, bytes):
return content.decode("utf-8", errors="replace")
return str(content or "")
async def _write(self, sandbox: Any, path: str, content: str) -> None:
fs = _require(sandbox, "fs", provider="daytona", where="sandbox")
write_fn = _require(fs, "upload_file", provider="daytona", where="sandbox.fs")
await write_fn(content.encode("utf-8"), path)
async def _pause(self, sandbox: Any) -> None:
stop_fn = _require(sandbox, "stop", provider="daytona", where="sandbox")
await stop_fn()
async def _resume(self, sandbox: Any) -> None:
start_fn = _require(sandbox, "start", provider="daytona", where="sandbox")
await start_fn()
async def _destroy(self, sandbox: Any) -> None:
client = await self._ensure_client()
delete_fn = _require(client, "delete", provider="daytona", where="AsyncDaytona")
await delete_fn(sandbox)
async def _preview_url(self, sandbox: Any, port: int) -> str:
link_fn = _require(sandbox, "get_preview_link", provider="daytona", where="sandbox")
link = await link_fn(port)
return str(getattr(link, "url", link))
class E2BSandboxAdapter(_RemoteSandboxAdapter):
"""E2B sandboxes via the async SDK.
Targets the documented surface: ``AsyncSandbox.create(...)``,
``sandbox.commands.run(...)``, ``sandbox.files.read/write``,
``sandbox.kill()``.
"""
provider_name = "e2b"
def __init__(self, *, api_key: str, max_output_bytes: int = 200_000) -> None:
super().__init__(max_output_bytes=max_output_bytes)
if not api_key:
raise ProviderUnavailableError(
"SANDBOX_PROVIDER=e2b نیازمند تنظیم E2B_API_KEY است."
)
self._api_key = api_key
self._sdk = _load_sdk("e2b")
self._preflight()
def _preflight(self) -> None:
_require(self._sdk, "AsyncSandbox", provider="e2b", where="e2b")
async def _ensure_client(self) -> Any:
# E2B has no long-lived client object; the API key is passed per call.
return self._sdk.AsyncSandbox
async def _create_sandbox(self, client: Any, *, image: str, template_id: str) -> Any:
create_fn = _require(client, "create", provider="e2b", where="AsyncSandbox")
return await create_fn(api_key=self._api_key)
async def _exec(self, sandbox: Any, *, command: str, timeout: int, env: dict[str, str]) -> Any:
commands = _require(sandbox, "commands", provider="e2b", where="sandbox")
run_fn = _require(commands, "run", provider="e2b", where="sandbox.commands")
result = await run_fn(command, timeout=timeout, envs=env or None)
return _E2BCommandOutput(
exit_code=int(getattr(result, "exit_code", 0) or 0),
stdout=str(getattr(result, "stdout", "") or ""),
stderr=str(getattr(result, "stderr", "") or ""),
)
async def _read(self, sandbox: Any, path: str) -> str:
files = _require(sandbox, "files", provider="e2b", where="sandbox")
read_fn = _require(files, "read", provider="e2b", where="sandbox.files")
content = await read_fn(path)
if isinstance(content, bytes):
return content.decode("utf-8", errors="replace")
return str(content or "")
async def _write(self, sandbox: Any, path: str, content: str) -> None:
files = _require(sandbox, "files", provider="e2b", where="sandbox")
write_fn = _require(files, "write", provider="e2b", where="sandbox.files")
await write_fn(path, content)
async def _pause(self, sandbox: Any) -> None:
pause_fn = _require(sandbox, "pause", provider="e2b", where="sandbox")
await pause_fn()
async def _resume(self, sandbox: Any) -> None:
# E2B resumes by reconnecting to the sandbox id.
connect_fn = _require(
self._sdk.AsyncSandbox, "connect", provider="e2b", where="AsyncSandbox"
)
await connect_fn(sandbox.sandbox_id, api_key=self._api_key)
async def _destroy(self, sandbox: Any) -> None:
kill_fn = _require(sandbox, "kill", provider="e2b", where="sandbox")
await kill_fn()
async def _preview_url(self, sandbox: Any, port: int) -> str:
host_fn = _require(sandbox, "get_host", provider="e2b", where="sandbox")
return f"https://{await host_fn(port)}"
class _DaytonaCommandOutput:
__slots__ = ("exit_code", "stdout", "stderr")
def __init__(self, *, exit_code: int, stdout: str, stderr: str) -> None:
self.exit_code = exit_code
self.stdout = stdout
self.stderr = stderr
class _E2BCommandOutput:
__slots__ = ("exit_code", "stdout", "stderr")
def __init__(self, *, exit_code: int, stdout: str, stderr: str) -> None:
self.exit_code = exit_code
self.stdout = stdout
self.stderr = stderr
def build_remote_sandbox_adapter(
provider: str, *, daytona_api_key: str = "", daytona_api_url: str = "", e2b_api_key: str = ""
):
"""Factory used by the adapter registry."""
normalised = (provider or "").strip().lower()
if normalised == "daytona":
return DaytonaSandboxAdapter(api_key=daytona_api_key, api_url=daytona_api_url)
if normalised == "e2b":
return E2BSandboxAdapter(api_key=e2b_api_key)
raise ProviderUnavailableError(
f"SANDBOX_PROVIDER='{provider}' پشتیبانی نمی‌شود. مقادیر مجاز: mock | daytona | e2b"
)