refactor(box): remove legacy in-process runtime code and clean up smells

After the architecture settled on always using an independent Box Runtime
service, several pieces of compatibility code and design shortcuts were
left behind. This commit cleans them up:

- Remove `LocalBoxRuntimeClient` and `create_box_runtime_client` from
  production code (moved to test-only helper).
- Remove unused `_clip_bytes` method from backend.
- Remove `__langbot_session_placeholder__` hack by making `BoxSpec.cmd`
  default to empty and validating non-empty only in `runtime.execute()`.
- Extract `get_box_config()` helper to eliminate 5× duplicated config
  access boilerplate.
- Remove `session_id`/`host_path`/`host_path_mode` from the LLM-facing
  tool schema to enforce request-scoped session isolation.
- Fix dual shutdown path: `NativeToolLoader.shutdown()` no longer calls
  `box_service.shutdown()` (handled by `Application.dispose()`).
- Simplify `_assert_session_compatible` with a loop.
- Inline client creation in `BoxRuntimeConnector`.
- Remove redundant `BOX__RUNTIME_URL` env var from docker-compose
  (auto-detected by code).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
youhuanghe
2026-03-20 12:04:39 +00:00
committed by WangCham
parent eaae31edd0
commit 6391678fdb
11 changed files with 98 additions and 161 deletions
-1
View File
@@ -45,7 +45,6 @@ services:
restart: on-failure restart: on-failure
environment: environment:
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
- BOX__RUNTIME_URL=http://langbot_box_runtime:5410
ports: ports:
- 5300:5300 # For web ui and webhook callback - 5300:5300 # For web ui and webhook callback
- 2280-2285:2280-2285 # For platform reverse connection - 2280-2285:2280-2285 # For platform reverse connection
-6
View File
@@ -248,12 +248,6 @@ class CLISandboxBackend(BaseSandboxBackend):
timed_out=False, timed_out=False,
) )
@staticmethod
def _clip_bytes(data: bytes, limit: int = _MAX_RAW_OUTPUT_BYTES) -> str:
"""Decode bytes to str, discarding bytes beyond *limit*."""
clipped = data[:limit]
return CLISandboxBackend._clip_captured_bytes(clipped, len(data), limit=limit)
@staticmethod @staticmethod
def _clip_captured_bytes(data: bytes, total_size: int, limit: int = _MAX_RAW_OUTPUT_BYTES) -> str: def _clip_captured_bytes(data: bytes, total_size: int, limit: int = _MAX_RAW_OUTPUT_BYTES) -> str:
text = data.decode('utf-8', errors='replace').strip() text = data.decode('utf-8', errors='replace').strip()
+3 -51
View File
@@ -1,4 +1,4 @@
"""BoxRuntimeClient abstraction for local and remote Box Runtime access.""" """BoxRuntimeClient abstraction for remote Box Runtime access."""
from __future__ import annotations from __future__ import annotations
@@ -16,8 +16,7 @@ from .errors import (
BoxSessionNotFoundError, BoxSessionNotFoundError,
BoxValidationError, BoxValidationError,
) )
from .models import BoxExecutionResult, BoxExecutionStatus, BoxSpec from .models import BoxExecutionResult, BoxExecutionStatus, BoxSpec, get_box_config
from .runtime import BoxRuntime
from ..utils import platform from ..utils import platform
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -34,9 +33,7 @@ _ERROR_CODE_MAP: dict[str, type[BoxError]] = {
def resolve_box_runtime_url(ap: 'core_app.Application') -> str: def resolve_box_runtime_url(ap: 'core_app.Application') -> str:
box_config = getattr(ap, 'instance_config', None) runtime_url = str(get_box_config(ap).get('runtime_url', '')).strip()
box_config_data = getattr(box_config, 'data', {}) if box_config is not None else {}
runtime_url = str(box_config_data.get('box', {}).get('runtime_url', '')).strip()
if runtime_url: if runtime_url:
return runtime_url return runtime_url
@@ -45,16 +42,6 @@ def resolve_box_runtime_url(ap: 'core_app.Application') -> str:
return 'http://127.0.0.1:5410' return 'http://127.0.0.1:5410'
def create_box_runtime_client(
ap: 'core_app.Application',
runtime_url: str | None = None,
) -> 'RemoteBoxRuntimeClient':
return RemoteBoxRuntimeClient(
base_url=runtime_url or resolve_box_runtime_url(ap),
logger=ap.logger,
)
class BoxRuntimeClient(abc.ABC): class BoxRuntimeClient(abc.ABC):
"""Abstract interface that BoxService uses to talk to a Box Runtime.""" """Abstract interface that BoxService uses to talk to a Box Runtime."""
@@ -83,41 +70,6 @@ class BoxRuntimeClient(abc.ABC):
async def create_session(self, spec: BoxSpec) -> dict: ... async def create_session(self, spec: BoxSpec) -> dict: ...
class LocalBoxRuntimeClient(BoxRuntimeClient):
"""In-process client that wraps a real BoxRuntime directly."""
def __init__(self, logger: logging.Logger, runtime: BoxRuntime | None = None):
self._runtime = runtime or BoxRuntime(logger=logger)
@property
def runtime(self) -> BoxRuntime:
return self._runtime
async def initialize(self) -> None:
await self._runtime.initialize()
async def execute(self, spec: BoxSpec) -> BoxExecutionResult:
return await self._runtime.execute(spec)
async def shutdown(self) -> None:
await self._runtime.shutdown()
async def get_status(self) -> dict:
return await self._runtime.get_status()
async def get_sessions(self) -> list[dict]:
return self._runtime.get_sessions()
async def get_backend_info(self) -> dict:
return await self._runtime.get_backend_info()
async def delete_session(self, session_id: str) -> None:
await self._runtime.delete_session(session_id)
async def create_session(self, spec: BoxSpec) -> dict:
return await self._runtime.create_session(spec)
class RemoteBoxRuntimeClient(BoxRuntimeClient): class RemoteBoxRuntimeClient(BoxRuntimeClient):
"""HTTP client that talks to a standalone Box Runtime service.""" """HTTP client that talks to a standalone Box Runtime service."""
+4 -5
View File
@@ -6,7 +6,8 @@ import sys
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .errors import BoxRuntimeUnavailableError from .errors import BoxRuntimeUnavailableError
from .client import create_box_runtime_client, resolve_box_runtime_url from .client import RemoteBoxRuntimeClient, resolve_box_runtime_url
from .models import get_box_config
from ..utils import platform from ..utils import platform
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -24,7 +25,7 @@ class BoxRuntimeConnector:
self.configured_runtime_url = self._load_configured_runtime_url() self.configured_runtime_url = self._load_configured_runtime_url()
self.runtime_url = self.configured_runtime_url or resolve_box_runtime_url(ap) self.runtime_url = self.configured_runtime_url or resolve_box_runtime_url(ap)
self.manages_local_runtime = self._should_manage_local_runtime() self.manages_local_runtime = self._should_manage_local_runtime()
self.client = create_box_runtime_client(ap, runtime_url=self.runtime_url) self.client = RemoteBoxRuntimeClient(base_url=self.runtime_url, logger=ap.logger)
self.runtime_subprocess: asyncio.subprocess.Process | None = None self.runtime_subprocess: asyncio.subprocess.Process | None = None
self.runtime_subprocess_task: asyncio.Task | None = None self.runtime_subprocess_task: asyncio.Task | None = None
@@ -54,9 +55,7 @@ class BoxRuntimeConnector:
self.runtime_subprocess_task = None self.runtime_subprocess_task = None
def _load_configured_runtime_url(self) -> str: def _load_configured_runtime_url(self) -> str:
box_config = getattr(self.ap, 'instance_config', None) return str(get_box_config(self.ap).get('runtime_url', '')).strip()
box_config_data = getattr(box_config, 'data', {}) if box_config is not None else {}
return str(box_config_data.get('box', {}).get('runtime_url', '')).strip()
def _should_manage_local_runtime(self) -> bool: def _should_manage_local_runtime(self) -> bool:
return not self.configured_runtime_url and platform.get_platform() != 'docker' return not self.configured_runtime_url and platform.get_platform() != 'docker'
+9 -5
View File
@@ -10,6 +10,13 @@ DEFAULT_BOX_IMAGE = 'python:3.11-slim'
DEFAULT_BOX_MOUNT_PATH = '/workspace' DEFAULT_BOX_MOUNT_PATH = '/workspace'
def get_box_config(ap) -> dict:
"""Return the 'box' section from instance config, with safe fallbacks."""
instance_config = getattr(ap, 'instance_config', None)
config_data = getattr(instance_config, 'data', {}) if instance_config is not None else {}
return config_data.get('box', {})
class BoxNetworkMode(str, enum.Enum): class BoxNetworkMode(str, enum.Enum):
OFF = 'off' OFF = 'off'
ON = 'on' ON = 'on'
@@ -26,7 +33,7 @@ class BoxHostMountMode(str, enum.Enum):
class BoxSpec(pydantic.BaseModel): class BoxSpec(pydantic.BaseModel):
cmd: str cmd: str = ''
workdir: str = '/workspace' workdir: str = '/workspace'
timeout_sec: int = 30 timeout_sec: int = 30
network: BoxNetworkMode = BoxNetworkMode.OFF network: BoxNetworkMode = BoxNetworkMode.OFF
@@ -44,10 +51,7 @@ class BoxSpec(pydantic.BaseModel):
@pydantic.field_validator('cmd') @pydantic.field_validator('cmd')
@classmethod @classmethod
def validate_cmd(cls, value: str) -> str: def validate_cmd(cls, value: str) -> str:
value = value.strip() return value.strip()
if not value:
raise ValueError('cmd must not be empty')
return value
@pydantic.field_validator('workdir') @pydantic.field_validator('workdir')
@classmethod @classmethod
+15 -33
View File
@@ -6,7 +6,7 @@ import datetime as dt
import logging import logging
from .backend import BaseSandboxBackend, DockerBackend, PodmanBackend from .backend import BaseSandboxBackend, DockerBackend, PodmanBackend
from .errors import BoxBackendUnavailableError, BoxSessionConflictError, BoxSessionNotFoundError from .errors import BoxBackendUnavailableError, BoxSessionConflictError, BoxSessionNotFoundError, BoxValidationError
from .models import BoxExecutionResult, BoxExecutionStatus, BoxSessionInfo, BoxSpec from .models import BoxExecutionResult, BoxExecutionStatus, BoxSessionInfo, BoxSpec
_UTC = dt.timezone.utc _UTC = dt.timezone.utc
@@ -36,6 +36,8 @@ class BoxRuntime:
self._backend = await self._select_backend() self._backend = await self._select_backend()
async def execute(self, spec: BoxSpec) -> BoxExecutionResult: async def execute(self, spec: BoxSpec) -> BoxExecutionResult:
if not spec.cmd:
raise BoxValidationError('cmd must not be empty')
session = await self._get_or_create_session(spec) session = await self._get_or_create_session(spec)
async with session.lock: async with session.lock:
@@ -183,38 +185,18 @@ class BoxRuntime:
self.logger.warning(f'Failed to clean up box session {session_id}: {exc}') self.logger.warning(f'Failed to clean up box session {session_id}: {exc}')
def _assert_session_compatible(self, session: BoxSessionInfo, spec: BoxSpec): def _assert_session_compatible(self, session: BoxSessionInfo, spec: BoxSpec):
if session.network != spec.network: _COMPAT_FIELDS = (
raise BoxSessionConflictError( 'network', 'image', 'host_path', 'host_path_mode',
f'sandbox_exec session {spec.session_id} already exists with network={session.network.value}' 'cpus', 'memory_mb', 'pids_limit', 'read_only_rootfs',
) )
if session.image != spec.image: for field in _COMPAT_FIELDS:
raise BoxSessionConflictError( session_val = getattr(session, field)
f'sandbox_exec session {spec.session_id} already exists with image={session.image}' spec_val = getattr(spec, field)
) if session_val != spec_val:
if session.host_path != spec.host_path: display = session_val.value if hasattr(session_val, 'value') else session_val
raise BoxSessionConflictError( raise BoxSessionConflictError(
f'sandbox_exec session {spec.session_id} already exists with host_path={session.host_path}' f'sandbox_exec session {spec.session_id} already exists with {field}={display}'
) )
if session.host_path_mode != spec.host_path_mode:
raise BoxSessionConflictError(
f'sandbox_exec session {spec.session_id} already exists with host_path_mode={session.host_path_mode.value}'
)
if session.cpus != spec.cpus:
raise BoxSessionConflictError(
f'sandbox_exec session {spec.session_id} already exists with cpus={session.cpus}'
)
if session.memory_mb != spec.memory_mb:
raise BoxSessionConflictError(
f'sandbox_exec session {spec.session_id} already exists with memory_mb={session.memory_mb}'
)
if session.pids_limit != spec.pids_limit:
raise BoxSessionConflictError(
f'sandbox_exec session {spec.session_id} already exists with pids_limit={session.pids_limit}'
)
if session.read_only_rootfs != spec.read_only_rootfs:
raise BoxSessionConflictError(
f'sandbox_exec session {spec.session_id} already exists with read_only_rootfs={session.read_only_rootfs}'
)
@staticmethod @staticmethod
def _session_to_dict(info: BoxSessionInfo) -> dict: def _session_to_dict(info: BoxSessionInfo) -> dict:
-1
View File
@@ -81,7 +81,6 @@ async def handle_create_session(request: web.Request) -> web.Response:
body = await request.json() body = await request.json()
session_id = request.match_info['session_id'] session_id = request.match_info['session_id']
body['session_id'] = session_id body['session_id'] = session_id
body.setdefault('cmd', '__langbot_session_placeholder__')
spec = BoxSpec.model_validate(body) spec = BoxSpec.model_validate(body)
session_info = await runtime.create_session(spec) session_info = await runtime.create_session(spec)
return web.json_response(session_info, status=201) return web.json_response(session_info, status=201)
+4 -10
View File
@@ -12,7 +12,7 @@ import pydantic
from .client import BoxRuntimeClient from .client import BoxRuntimeClient
from .connector import BoxRuntimeConnector from .connector import BoxRuntimeConnector
from .errors import BoxError, BoxValidationError from .errors import BoxError, BoxValidationError
from .models import BUILTIN_PROFILES, BoxExecutionResult, BoxProfile, BoxSpec from .models import BUILTIN_PROFILES, BoxExecutionResult, BoxProfile, BoxSpec, get_box_config
_INT_ADAPTER = pydantic.TypeAdapter(int) _INT_ADAPTER = pydantic.TypeAdapter(int)
_UTC = _dt.timezone.utc _UTC = _dt.timezone.utc
@@ -189,9 +189,7 @@ class BoxService:
} }
def _load_allowed_host_mount_roots(self) -> list[str]: def _load_allowed_host_mount_roots(self) -> list[str]:
box_config = getattr(self.ap, 'instance_config', None) configured_roots = get_box_config(self.ap).get('allowed_host_mount_roots', [])
box_config_data = getattr(box_config, 'data', {}) if box_config is not None else {}
configured_roots = box_config_data.get('box', {}).get('allowed_host_mount_roots', [])
normalized_roots: list[str] = [] normalized_roots: list[str] = []
for root in configured_roots: for root in configured_roots:
@@ -203,9 +201,7 @@ class BoxService:
return normalized_roots return normalized_roots
def _load_default_host_workspace(self) -> str | None: def _load_default_host_workspace(self) -> str | None:
box_config = getattr(self.ap, 'instance_config', None) default_host_workspace = str(get_box_config(self.ap).get('default_host_workspace', '')).strip()
box_config_data = getattr(box_config, 'data', {}) if box_config is not None else {}
default_host_workspace = str(box_config_data.get('box', {}).get('default_host_workspace', '')).strip()
if not default_host_workspace: if not default_host_workspace:
return None return None
return os.path.realpath(os.path.abspath(default_host_workspace)) return os.path.realpath(os.path.abspath(default_host_workspace))
@@ -252,9 +248,7 @@ class BoxService:
raise BoxValidationError(f'host_path is outside allowed_host_mount_roots: {allowed_roots}') raise BoxValidationError(f'host_path is outside allowed_host_mount_roots: {allowed_roots}')
def _load_profile(self) -> BoxProfile: def _load_profile(self) -> BoxProfile:
box_config = getattr(self.ap, 'instance_config', None) profile_name = str(get_box_config(self.ap).get('profile', 'default')).strip() or 'default'
box_config_data = getattr(box_config, 'data', {}) if box_config is not None else {}
profile_name = str(box_config_data.get('box', {}).get('profile', 'default')).strip() or 'default'
profile = BUILTIN_PROFILES.get(profile_name) profile = BUILTIN_PROFILES.get(profile_name)
if profile is None: if profile is None:
@@ -28,8 +28,7 @@ class NativeToolLoader(loader.ToolLoader):
return await self.ap.box_service.execute_sandbox_tool(parameters, query) return await self.ap.box_service.execute_sandbox_tool(parameters, query)
async def shutdown(self): async def shutdown(self):
if getattr(self.ap, 'box_service', None) is not None: pass
await self.ap.box_service.shutdown()
def _build_sandbox_exec_tool(self) -> resource_tool.LLMTool: def _build_sandbox_exec_tool(self) -> resource_tool.LLMTool:
return resource_tool.LLMTool( return resource_tool.LLMTool(
@@ -64,23 +63,6 @@ class NativeToolLoader(loader.ToolLoader):
'enum': ['off', 'on'], 'enum': ['off', 'on'],
'default': 'off', 'default': 'off',
}, },
'session_id': {
'type': 'string',
'description': 'Optional sandbox session id. Defaults to the current request id for reuse.',
},
'host_path': {
'type': 'string',
'description': (
'Optional absolute host directory path to mount into the sandbox as /workspace. '
'The path must be under an allowed host mount root.'
),
},
'host_path_mode': {
'type': 'string',
'description': 'Mount mode for host_path. Use rw to create or modify host files.',
'enum': ['ro', 'rw'],
'default': 'rw',
},
'env': { 'env': {
'type': 'object', 'type': 'object',
'description': 'Optional environment variables to expose inside the sandbox.', 'description': 'Optional environment variables to expose inside the sandbox.',
+8 -7
View File
@@ -5,33 +5,34 @@ import pytest
from langbot.pkg.box.backend import CLISandboxBackend, _MAX_RAW_OUTPUT_BYTES from langbot.pkg.box.backend import CLISandboxBackend, _MAX_RAW_OUTPUT_BYTES
class TestClipBytes: class TestClipCapturedBytes:
def test_within_limit_unchanged(self): def test_within_limit_unchanged(self):
data = b'hello world' data = b'hello world'
result = CLISandboxBackend._clip_bytes(data, limit=1024) result = CLISandboxBackend._clip_captured_bytes(data, total_size=len(data), limit=1024)
assert result == 'hello world' assert result == 'hello world'
def test_exceeding_limit_clips_and_appends_notice(self): def test_exceeding_limit_clips_and_appends_notice(self):
data = b'A' * 200 captured = b'A' * 100
result = CLISandboxBackend._clip_bytes(data, limit=100) total_size = 200
result = CLISandboxBackend._clip_captured_bytes(captured, total_size=total_size, limit=100)
assert result.startswith('A' * 100) assert result.startswith('A' * 100)
assert 'raw output clipped at 100 bytes' in result assert 'raw output clipped at 100 bytes' in result
assert '100 bytes discarded' in result assert '100 bytes discarded' in result
def test_exact_limit_not_clipped(self): def test_exact_limit_not_clipped(self):
data = b'B' * 100 data = b'B' * 100
result = CLISandboxBackend._clip_bytes(data, limit=100) result = CLISandboxBackend._clip_captured_bytes(data, total_size=100, limit=100)
assert result == 'B' * 100 assert result == 'B' * 100
assert 'clipped' not in result assert 'clipped' not in result
def test_default_limit_is_module_constant(self): def test_default_limit_is_module_constant(self):
data = b'x' * 10 data = b'x' * 10
result = CLISandboxBackend._clip_bytes(data) result = CLISandboxBackend._clip_captured_bytes(data, total_size=10)
assert result == 'x' * 10 assert result == 'x' * 10
assert _MAX_RAW_OUTPUT_BYTES == 1_048_576 assert _MAX_RAW_OUTPUT_BYTES == 1_048_576
def test_invalid_utf8_replaced(self): def test_invalid_utf8_replaced(self):
data = b'ok\xff\xfetail' data = b'ok\xff\xfetail'
result = CLISandboxBackend._clip_bytes(data, limit=1024) result = CLISandboxBackend._clip_captured_bytes(data, total_size=len(data), limit=1024)
assert 'ok' in result assert 'ok' in result
assert 'tail' in result assert 'tail' in result
+54 -23
View File
@@ -12,7 +12,7 @@ import pytest
import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query import langbot_plugin.api.entities.builtin.pipeline.query as pipeline_query
from langbot.pkg.box.backend import BaseSandboxBackend from langbot.pkg.box.backend import BaseSandboxBackend
from langbot.pkg.box.client import LocalBoxRuntimeClient, RemoteBoxRuntimeClient from langbot.pkg.box.client import BoxRuntimeClient, RemoteBoxRuntimeClient
from langbot.pkg.box.errors import BoxBackendUnavailableError, BoxSessionConflictError, BoxSessionNotFoundError, BoxValidationError from langbot.pkg.box.errors import BoxBackendUnavailableError, BoxSessionConflictError, BoxSessionNotFoundError, BoxValidationError
from langbot.pkg.box.models import ( from langbot.pkg.box.models import (
BUILTIN_PROFILES, BUILTIN_PROFILES,
@@ -30,6 +30,37 @@ from langbot.pkg.box.service import BoxService
_UTC = dt.timezone.utc _UTC = dt.timezone.utc
class _InProcessBoxRuntimeClient(BoxRuntimeClient):
"""Test-only client that wraps a BoxRuntime in-process (no HTTP)."""
def __init__(self, logger, runtime=None):
self._runtime = runtime or BoxRuntime(logger=logger)
async def initialize(self):
await self._runtime.initialize()
async def execute(self, spec):
return await self._runtime.execute(spec)
async def shutdown(self):
await self._runtime.shutdown()
async def get_status(self):
return await self._runtime.get_status()
async def get_sessions(self):
return self._runtime.get_sessions()
async def get_backend_info(self):
return await self._runtime.get_backend_info()
async def delete_session(self, session_id):
await self._runtime.delete_session(session_id)
async def create_session(self, spec):
return await self._runtime.create_session(spec)
def _can_open_test_socket() -> bool: def _can_open_test_socket() -> bool:
try: try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -197,7 +228,7 @@ async def test_box_service_defaults_session_id_from_query():
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool({'cmd': 'pwd', 'network': BoxNetworkMode.OFF.value}, make_query(7)) result = await service.execute_sandbox_tool({'cmd': 'pwd', 'network': BoxNetworkMode.OFF.value}, make_query(7))
@@ -212,7 +243,7 @@ async def test_box_service_fails_closed_when_backend_unavailable():
logger = Mock() logger = Mock()
backend = FakeBackend(logger, available=False) backend = FakeBackend(logger, available=False)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
with pytest.raises(BoxBackendUnavailableError): with pytest.raises(BoxBackendUnavailableError):
@@ -226,7 +257,7 @@ async def test_box_service_allows_host_mount_under_configured_root(tmp_path):
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
host_dir = tmp_path / 'mounted-workspace' host_dir = tmp_path / 'mounted-workspace'
host_dir.mkdir() host_dir.mkdir()
service = BoxService(make_app(logger, [str(tmp_path)]), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger, [str(tmp_path)]), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool( result = await service.execute_sandbox_tool(
@@ -251,7 +282,7 @@ async def test_box_service_uses_default_host_workspace_when_host_path_omitted(tm
host_dir.mkdir() host_dir.mkdir()
app = make_app(logger, [str(tmp_path)]) app = make_app(logger, [str(tmp_path)])
app.instance_config.data['box']['default_host_workspace'] = str(host_dir) app.instance_config.data['box']['default_host_workspace'] = str(host_dir)
service = BoxService(app, client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(app, client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool({'cmd': 'pwd'}, make_query(15)) result = await service.execute_sandbox_tool({'cmd': 'pwd'}, make_query(15))
@@ -272,7 +303,7 @@ async def test_box_service_creates_default_host_workspace_on_initialize(tmp_path
default_host_workspace = allowed_root / 'default-workspace' default_host_workspace = allowed_root / 'default-workspace'
app = make_app(logger, [str(allowed_root)]) app = make_app(logger, [str(allowed_root)])
app.instance_config.data['box']['default_host_workspace'] = str(default_host_workspace) app.instance_config.data['box']['default_host_workspace'] = str(default_host_workspace)
service = BoxService(app, client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(app, client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
@@ -288,7 +319,7 @@ async def test_box_service_rejects_host_mount_outside_allowed_roots(tmp_path):
disallowed_root = tmp_path / 'disallowed' disallowed_root = tmp_path / 'disallowed'
allowed_root.mkdir() allowed_root.mkdir()
disallowed_root.mkdir() disallowed_root.mkdir()
service = BoxService(make_app(logger, [str(allowed_root)]), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger, [str(allowed_root)]), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
with pytest.raises(BoxValidationError): with pytest.raises(BoxValidationError):
@@ -379,7 +410,7 @@ async def test_truncate_short_output_unchanged():
logger = Mock() logger = Mock()
backend = FakeBackendWithOutput(logger, stdout='hello world') backend = FakeBackendWithOutput(logger, stdout='hello world')
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime), output_limit_chars=100) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime), output_limit_chars=100)
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool({'cmd': 'echo hello'}, make_query(20)) result = await service.execute_sandbox_tool({'cmd': 'echo hello'}, make_query(20))
@@ -400,7 +431,7 @@ async def test_truncate_preserves_head_and_tail():
backend = FakeBackendWithOutput(logger, stdout=big_output) backend = FakeBackendWithOutput(logger, stdout=big_output)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
limit = 100 limit = 100
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime), output_limit_chars=limit) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime), output_limit_chars=limit)
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool({'cmd': 'cat big'}, make_query(21)) result = await service.execute_sandbox_tool({'cmd': 'cat big'}, make_query(21))
@@ -422,7 +453,7 @@ async def test_truncate_at_exact_limit_not_truncated():
exact_output = 'a' * 200 exact_output = 'a' * 200
backend = FakeBackendWithOutput(logger, stdout=exact_output) backend = FakeBackendWithOutput(logger, stdout=exact_output)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime), output_limit_chars=200) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime), output_limit_chars=200)
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool({'cmd': 'echo a'}, make_query(22)) result = await service.execute_sandbox_tool({'cmd': 'echo a'}, make_query(22))
@@ -436,7 +467,7 @@ async def test_truncate_stderr_independently():
logger = Mock() logger = Mock()
backend = FakeBackendWithOutput(logger, stdout='short', stderr='E' * 300) backend = FakeBackendWithOutput(logger, stdout='short', stderr='E' * 300)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime), output_limit_chars=100) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime), output_limit_chars=100)
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool({'cmd': 'fail'}, make_query(23)) result = await service.execute_sandbox_tool({'cmd': 'fail'}, make_query(23))
@@ -456,7 +487,7 @@ async def test_profile_default_provides_defaults():
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool({'cmd': 'echo hi'}, make_query(30)) result = await service.execute_sandbox_tool({'cmd': 'echo hi'}, make_query(30))
@@ -474,7 +505,7 @@ async def test_profile_unlocked_field_can_be_overridden():
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool( result = await service.execute_sandbox_tool(
@@ -494,7 +525,7 @@ async def test_profile_locked_field_cannot_be_overridden():
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger, profile='offline_readonly'), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger, profile='offline_readonly'), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool( result = await service.execute_sandbox_tool(
@@ -514,7 +545,7 @@ async def test_profile_timeout_clamped_to_max():
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
result = await service.execute_sandbox_tool( result = await service.execute_sandbox_tool(
@@ -534,7 +565,7 @@ async def test_profile_timeout_clamped_for_coercible_inputs(timeout_value):
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
await service.execute_sandbox_tool( await service.execute_sandbox_tool(
@@ -551,7 +582,7 @@ def test_unknown_profile_raises_error():
logger = Mock() logger = Mock()
runtime = BoxRuntime(logger=logger, backends=[FakeBackend(logger)], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[FakeBackend(logger)], session_ttl_sec=300)
with pytest.raises(BoxValidationError, match='unknown box profile'): with pytest.raises(BoxValidationError, match='unknown box profile'):
BoxService(make_app(logger, profile='nonexistent'), client=LocalBoxRuntimeClient(logger, runtime)) BoxService(make_app(logger, profile='nonexistent'), client=_InProcessBoxRuntimeClient(logger, runtime))
def test_builtin_profiles_are_consistent(): def test_builtin_profiles_are_consistent():
@@ -586,7 +617,7 @@ async def test_profile_default_applies_resource_limits():
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
await service.execute_sandbox_tool({'cmd': 'echo hi'}, make_query(40)) await service.execute_sandbox_tool({'cmd': 'echo hi'}, make_query(40))
@@ -605,7 +636,7 @@ async def test_profile_offline_readonly_locks_read_only_rootfs():
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger, profile='offline_readonly'), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger, profile='offline_readonly'), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
await service.execute_sandbox_tool( await service.execute_sandbox_tool(
@@ -623,7 +654,7 @@ async def test_profile_network_extended_has_relaxed_limits():
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger, profile='network_extended'), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger, profile='network_extended'), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
await service.execute_sandbox_tool({'cmd': 'echo hi'}, make_query(42)) await service.execute_sandbox_tool({'cmd': 'echo hi'}, make_query(42))
@@ -698,7 +729,7 @@ async def test_service_records_errors_on_failure():
logger = Mock() logger = Mock()
backend = FakeBackend(logger, available=False) backend = FakeBackend(logger, available=False)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
with pytest.raises(Exception): with pytest.raises(Exception):
@@ -716,7 +747,7 @@ async def test_service_error_ring_buffer_capped():
logger = Mock() logger = Mock()
backend = FakeBackend(logger, available=False) backend = FakeBackend(logger, available=False)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
for i in range(60): for i in range(60):
@@ -735,7 +766,7 @@ async def test_service_get_status_aggregates_runtime_and_profile():
logger = Mock() logger = Mock()
backend = FakeBackend(logger) backend = FakeBackend(logger)
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300) runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
service = BoxService(make_app(logger), client=LocalBoxRuntimeClient(logger, runtime)) service = BoxService(make_app(logger), client=_InProcessBoxRuntimeClient(logger, runtime))
await service.initialize() await service.initialize()
status = await service.get_status() status = await service.get_status()