mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-09 15:26:03 +00:00
fix: ruff
This commit is contained in:
@@ -8,14 +8,14 @@ from langbot_plugin.entities.io.actions.enums import ActionType
|
||||
class LangBotToBoxAction(ActionType):
|
||||
"""Actions sent from LangBot to the Box runtime."""
|
||||
|
||||
HEALTH = "box_health"
|
||||
STATUS = "box_status"
|
||||
EXEC = "box_exec"
|
||||
CREATE_SESSION = "box_create_session"
|
||||
GET_SESSION = "box_get_session"
|
||||
GET_SESSIONS = "box_get_sessions"
|
||||
DELETE_SESSION = "box_delete_session"
|
||||
START_MANAGED_PROCESS = "box_start_managed_process"
|
||||
GET_MANAGED_PROCESS = "box_get_managed_process"
|
||||
GET_BACKEND_INFO = "box_get_backend_info"
|
||||
SHUTDOWN = "box_shutdown"
|
||||
HEALTH = 'box_health'
|
||||
STATUS = 'box_status'
|
||||
EXEC = 'box_exec'
|
||||
CREATE_SESSION = 'box_create_session'
|
||||
GET_SESSION = 'box_get_session'
|
||||
GET_SESSIONS = 'box_get_sessions'
|
||||
DELETE_SESSION = 'box_delete_session'
|
||||
START_MANAGED_PROCESS = 'box_start_managed_process'
|
||||
GET_MANAGED_PROCESS = 'box_get_managed_process'
|
||||
GET_BACKEND_INFO = 'box_get_backend_info'
|
||||
SHUTDOWN = 'box_shutdown'
|
||||
|
||||
@@ -11,7 +11,15 @@ import shutil
|
||||
import uuid
|
||||
|
||||
from .errors import BoxError
|
||||
from .models import DEFAULT_BOX_MOUNT_PATH, BoxExecutionResult, BoxExecutionStatus, BoxHostMountMode, BoxNetworkMode, BoxSessionInfo, BoxSpec
|
||||
from .models import (
|
||||
DEFAULT_BOX_MOUNT_PATH,
|
||||
BoxExecutionResult,
|
||||
BoxExecutionStatus,
|
||||
BoxHostMountMode,
|
||||
BoxNetworkMode,
|
||||
BoxSessionInfo,
|
||||
BoxSpec,
|
||||
)
|
||||
from .security import validate_sandbox_security
|
||||
|
||||
# Hard cap on raw subprocess output to prevent unbounded memory usage.
|
||||
@@ -213,8 +221,15 @@ class CLISandboxBackend(BaseSandboxBackend):
|
||||
older versions) are also removed.
|
||||
"""
|
||||
result = await self._run_command(
|
||||
[self.command, 'ps', '-a', '--filter', 'label=langbot.box=true',
|
||||
'--format', '{{.ID}}\t{{.Label "langbot.box.instance_id"}}'],
|
||||
[
|
||||
self.command,
|
||||
'ps',
|
||||
'-a',
|
||||
'--filter',
|
||||
'label=langbot.box=true',
|
||||
'--format',
|
||||
'{{.ID}}\t{{.Label "langbot.box.instance_id"}}',
|
||||
],
|
||||
timeout_sec=10,
|
||||
check=False,
|
||||
)
|
||||
|
||||
@@ -82,6 +82,7 @@ def _translate_action_error(exc: Exception) -> BoxError:
|
||||
BoxSessionNotFoundError,
|
||||
BoxValidationError,
|
||||
)
|
||||
|
||||
msg = str(exc)
|
||||
_ERROR_PREFIX_MAP: list[tuple[str, type[BoxError]]] = [
|
||||
('BoxValidationError:', BoxValidationError),
|
||||
@@ -182,10 +183,10 @@ class ActionRPCBoxClient(BoxRuntimeClient):
|
||||
base = ws_relay_base_url
|
||||
if base.startswith('https://'):
|
||||
scheme = 'wss://'
|
||||
suffix = base[len('https://'):]
|
||||
suffix = base[len('https://') :]
|
||||
elif base.startswith('http://'):
|
||||
scheme = 'ws://'
|
||||
suffix = base[len('http://'):]
|
||||
suffix = base[len('http://') :]
|
||||
else:
|
||||
scheme = 'ws://'
|
||||
suffix = base
|
||||
|
||||
@@ -46,7 +46,10 @@ class BoxRuntimeConnector:
|
||||
await self._connect_remote_ws()
|
||||
|
||||
def _make_connection_callback(
|
||||
self, transport_name: str, connected: asyncio.Event, connect_error: list[Exception],
|
||||
self,
|
||||
transport_name: str,
|
||||
connected: asyncio.Event,
|
||||
connect_error: list[Exception],
|
||||
):
|
||||
async def new_connection_callback(connection: Connection) -> None:
|
||||
handler = Handler(connection)
|
||||
|
||||
@@ -174,9 +174,7 @@ class BoxRuntime:
|
||||
raise BoxSessionNotFoundError(f'session {session_id} not found')
|
||||
result = self._session_to_dict(runtime_session.info)
|
||||
if runtime_session.managed_process is not None:
|
||||
result['managed_process'] = self._managed_process_to_dict(
|
||||
session_id, runtime_session.managed_process
|
||||
)
|
||||
result['managed_process'] = self._managed_process_to_dict(session_id, runtime_session.managed_process)
|
||||
return result
|
||||
|
||||
async def get_status(self) -> dict:
|
||||
@@ -281,8 +279,14 @@ class BoxRuntime:
|
||||
|
||||
def _assert_session_compatible(self, session: BoxSessionInfo, spec: BoxSpec):
|
||||
_COMPAT_FIELDS = (
|
||||
'network', 'image', 'host_path', 'host_path_mode',
|
||||
'cpus', 'memory_mb', 'pids_limit', 'read_only_rootfs',
|
||||
'network',
|
||||
'image',
|
||||
'host_path',
|
||||
'host_path_mode',
|
||||
'cpus',
|
||||
'memory_mb',
|
||||
'pids_limit',
|
||||
'read_only_rootfs',
|
||||
)
|
||||
for field in _COMPAT_FIELDS:
|
||||
session_val = getattr(session, field)
|
||||
@@ -308,7 +312,10 @@ class BoxRuntime:
|
||||
continue
|
||||
managed_process.stderr_chunks.append(text)
|
||||
managed_process.stderr_total_len += len(text) + 1 # +1 for '\n' separator
|
||||
while managed_process.stderr_total_len > _MANAGED_PROCESS_STDERR_PREVIEW_LIMIT and managed_process.stderr_chunks:
|
||||
while (
|
||||
managed_process.stderr_total_len > _MANAGED_PROCESS_STDERR_PREVIEW_LIMIT
|
||||
and managed_process.stderr_chunks
|
||||
):
|
||||
removed = managed_process.stderr_chunks.popleft()
|
||||
managed_process.stderr_total_len -= len(removed) + 1
|
||||
self.logger.info(f'LangBot Box managed process stderr: session_id={session_id} {text}')
|
||||
@@ -322,10 +329,7 @@ class BoxRuntime:
|
||||
runtime_session = self._sessions.get(session_id)
|
||||
if runtime_session is not None:
|
||||
runtime_session.info.last_used_at = managed_process.exited_at
|
||||
self.logger.info(
|
||||
'LangBot Box managed process exited: '
|
||||
f'session_id={session_id} return_code={return_code}'
|
||||
)
|
||||
self.logger.info(f'LangBot Box managed process exited: session_id={session_id} return_code={return_code}')
|
||||
|
||||
async def _terminate_managed_process(self, runtime_session: _RuntimeSession) -> None:
|
||||
managed_process = runtime_session.managed_process
|
||||
|
||||
@@ -5,20 +5,22 @@ import os
|
||||
from .errors import BoxValidationError
|
||||
from .models import BoxSpec
|
||||
|
||||
BLOCKED_HOST_PATHS = frozenset({
|
||||
'/etc',
|
||||
'/proc',
|
||||
'/sys',
|
||||
'/dev',
|
||||
'/root',
|
||||
'/boot',
|
||||
'/run',
|
||||
'/var/run',
|
||||
'/run/docker.sock',
|
||||
'/var/run/docker.sock',
|
||||
'/run/podman',
|
||||
'/var/run/podman',
|
||||
})
|
||||
BLOCKED_HOST_PATHS = frozenset(
|
||||
{
|
||||
'/etc',
|
||||
'/proc',
|
||||
'/sys',
|
||||
'/dev',
|
||||
'/root',
|
||||
'/boot',
|
||||
'/run',
|
||||
'/var/run',
|
||||
'/run/docker.sock',
|
||||
'/var/run/docker.sock',
|
||||
'/run/podman',
|
||||
'/var/run/podman',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def validate_sandbox_security(spec: BoxSpec) -> None:
|
||||
@@ -30,6 +32,4 @@ def validate_sandbox_security(spec: BoxSpec) -> None:
|
||||
real = os.path.realpath(spec.host_path)
|
||||
for blocked in BLOCKED_HOST_PATHS:
|
||||
if real == blocked or real.startswith(blocked + '/'):
|
||||
raise BoxValidationError(
|
||||
f'host_path {spec.host_path} is blocked for security'
|
||||
)
|
||||
raise BoxValidationError(f'host_path {spec.host_path} is blocked for security')
|
||||
|
||||
@@ -51,7 +51,6 @@ class BoxServerHandler(Handler):
|
||||
self._register_actions()
|
||||
|
||||
def _register_actions(self) -> None:
|
||||
|
||||
@self.action(CommonAction.PING)
|
||||
async def ping(data: dict[str, Any]) -> ActionResponse:
|
||||
return ActionResponse.success({})
|
||||
@@ -109,9 +108,7 @@ class BoxServerHandler(Handler):
|
||||
|
||||
@self.action(LangBotToBoxAction.GET_MANAGED_PROCESS)
|
||||
async def get_managed_process(data: dict[str, Any]) -> ActionResponse:
|
||||
return ActionResponse.success(
|
||||
self._runtime.get_managed_process(data['session_id'])
|
||||
)
|
||||
return ActionResponse.success(self._runtime.get_managed_process(data['session_id']))
|
||||
|
||||
@self.action(LangBotToBoxAction.GET_BACKEND_INFO)
|
||||
async def get_backend_info(data: dict[str, Any]) -> ActionResponse:
|
||||
@@ -146,7 +143,9 @@ async def handle_managed_process_ws(request: web.Request) -> web.StreamResponse:
|
||||
if managed_process is None:
|
||||
return _error_response(BoxManagedProcessNotFoundError(f'session {session_id} has no managed process'))
|
||||
if not managed_process.is_running:
|
||||
return _error_response(BoxManagedProcessConflictError(f'managed process in session {session_id} is not running'))
|
||||
return _error_response(
|
||||
BoxManagedProcessConflictError(f'managed process in session {session_id} is not running')
|
||||
)
|
||||
|
||||
ws = web.WebSocketResponse(protocols=('mcp',))
|
||||
await ws.prepare(request)
|
||||
@@ -173,7 +172,12 @@ async def handle_managed_process_ws(request: web.Request) -> web.StreamResponse:
|
||||
stdin.write((msg.data + '\n').encode('utf-8'))
|
||||
await stdin.drain()
|
||||
runtime_session.info.last_used_at = dt.datetime.now(dt.timezone.utc)
|
||||
elif msg.type in (web.WSMsgType.CLOSE, web.WSMsgType.CLOSING, web.WSMsgType.CLOSED, web.WSMsgType.ERROR):
|
||||
elif msg.type in (
|
||||
web.WSMsgType.CLOSE,
|
||||
web.WSMsgType.CLOSING,
|
||||
web.WSMsgType.CLOSED,
|
||||
web.WSMsgType.ERROR,
|
||||
):
|
||||
break
|
||||
|
||||
stdout_task = asyncio.create_task(_stdout_to_ws())
|
||||
@@ -229,10 +233,12 @@ async def _run_server(host: str, port: int, mode: str) -> None:
|
||||
try:
|
||||
if mode == 'stdio':
|
||||
from langbot_plugin.runtime.io.controllers.stdio.server import StdioServerController
|
||||
|
||||
ctrl = StdioServerController()
|
||||
await ctrl.run(new_connection_callback)
|
||||
else:
|
||||
from langbot_plugin.runtime.io.controllers.ws.server import WebSocketServerController
|
||||
|
||||
# Action RPC uses port+1 to avoid conflict with ws relay
|
||||
rpc_port = port + 1
|
||||
logger.info(f'Box action RPC (ws) listening on {host}:{rpc_port}')
|
||||
@@ -248,8 +254,9 @@ def main() -> None:
|
||||
parser = argparse.ArgumentParser(description='LangBot Box Runtime Service')
|
||||
parser.add_argument('--host', default='0.0.0.0', help='Bind address')
|
||||
parser.add_argument('--port', type=int, default=5410, help='Bind port (ws relay)')
|
||||
parser.add_argument('--mode', choices=['stdio', 'ws'], default='stdio',
|
||||
help='Control channel transport (default: stdio)')
|
||||
parser.add_argument(
|
||||
'--mode', choices=['stdio', 'ws'], default='stdio', help='Control channel transport (default: stdio)'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
|
||||
|
||||
@@ -31,6 +31,7 @@ def _is_path_under(path: str, root: str) -> bool:
|
||||
"""Check whether *path* equals *root* or is a child of *root*."""
|
||||
return path == root or path.startswith(f'{root}{os.sep}')
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..core import app as core_app
|
||||
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
|
||||
@@ -66,9 +67,7 @@ class BoxService:
|
||||
await self.client.initialize()
|
||||
self._available = True
|
||||
except Exception as exc:
|
||||
self.ap.logger.warning(
|
||||
f'LangBot Box runtime unavailable, sandbox features disabled: {exc}'
|
||||
)
|
||||
self.ap.logger.warning(f'LangBot Box runtime unavailable, sandbox features disabled: {exc}')
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
@@ -109,11 +108,7 @@ class BoxService:
|
||||
if self._runtime_connector is not None:
|
||||
self._runtime_connector.dispose()
|
||||
loop = getattr(self.ap, 'event_loop', None)
|
||||
if (
|
||||
loop is not None
|
||||
and not loop.is_closed()
|
||||
and (self._shutdown_task is None or self._shutdown_task.done())
|
||||
):
|
||||
if loop is not None and not loop.is_closed() and (self._shutdown_task is None or self._shutdown_task.done()):
|
||||
self._shutdown_task = loop.create_task(self.shutdown())
|
||||
|
||||
async def get_sessions(self) -> list[dict]:
|
||||
@@ -295,7 +290,9 @@ class BoxService:
|
||||
raise BoxValidationError('host_path must point to an existing directory on the host')
|
||||
|
||||
if not self.allowed_host_mount_roots:
|
||||
raise BoxValidationError('host_path mounting is disabled because no allowed_host_mount_roots are configured')
|
||||
raise BoxValidationError(
|
||||
'host_path mounting is disabled because no allowed_host_mount_roots are configured'
|
||||
)
|
||||
|
||||
for allowed_root in self.allowed_host_mount_roots:
|
||||
if _is_path_under(host_path, allowed_root):
|
||||
@@ -317,8 +314,14 @@ class BoxService:
|
||||
"""Merge profile defaults into *params* in-place, enforce locked fields and clamp timeout."""
|
||||
profile = self.profile
|
||||
_PROFILE_FIELDS = (
|
||||
'image', 'network', 'timeout_sec', 'host_path_mode',
|
||||
'cpus', 'memory_mb', 'pids_limit', 'read_only_rootfs',
|
||||
'image',
|
||||
'network',
|
||||
'timeout_sec',
|
||||
'host_path_mode',
|
||||
'cpus',
|
||||
'memory_mb',
|
||||
'pids_limit',
|
||||
'read_only_rootfs',
|
||||
)
|
||||
|
||||
for field in _PROFILE_FIELDS:
|
||||
@@ -342,12 +345,14 @@ class BoxService:
|
||||
# ── Observability ─────────────────────────────────────────────────
|
||||
|
||||
def _record_error(self, exc: Exception, query: 'pipeline_query.Query'):
|
||||
self._recent_errors.append({
|
||||
'timestamp': _dt.datetime.now(_UTC).isoformat(),
|
||||
'type': type(exc).__name__,
|
||||
'message': str(exc),
|
||||
'query_id': str(query.query_id),
|
||||
})
|
||||
self._recent_errors.append(
|
||||
{
|
||||
'timestamp': _dt.datetime.now(_UTC).isoformat(),
|
||||
'type': type(exc).__name__,
|
||||
'message': str(exc),
|
||||
'query_id': str(query.query_id),
|
||||
}
|
||||
)
|
||||
|
||||
def get_recent_errors(self) -> list[dict]:
|
||||
return list(self._recent_errors)
|
||||
|
||||
Reference in New Issue
Block a user