mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-12 08:46:02 +00:00
fix(box): restore sandbox config and shared mcp runtime
This commit is contained in:
@@ -297,9 +297,14 @@ async def test_full_service_to_remote_runtime(tmp_path):
|
||||
instance_config=SimpleNamespace(
|
||||
data={
|
||||
'box': {
|
||||
'profile': 'default',
|
||||
'allowed_host_mount_roots': [str(tmp_path)],
|
||||
'default_host_workspace': str(host_dir),
|
||||
'backend': 'local',
|
||||
'runtime': {'endpoint': ''},
|
||||
'local': {
|
||||
'profile': 'default',
|
||||
'allowed_mount_roots': [str(tmp_path)],
|
||||
'default_workspace': str(host_dir),
|
||||
},
|
||||
'e2b': {'api_key': '', 'api_url': '', 'template': ''},
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
@@ -9,16 +9,20 @@ from langbot_plugin.box.client import ActionRPCBoxClient
|
||||
from langbot.pkg.box.connector import BoxRuntimeConnector
|
||||
|
||||
|
||||
def make_app(logger: Mock, runtime_url: str = ''):
|
||||
def make_app(logger: Mock, runtime_endpoint: str = ''):
|
||||
return SimpleNamespace(
|
||||
logger=logger,
|
||||
instance_config=SimpleNamespace(
|
||||
data={
|
||||
'box': {
|
||||
'runtime_url': runtime_url,
|
||||
'profile': 'default',
|
||||
'allowed_host_mount_roots': [],
|
||||
'default_host_workspace': '',
|
||||
'backend': 'local',
|
||||
'runtime': {'endpoint': runtime_endpoint},
|
||||
'local': {
|
||||
'profile': 'default',
|
||||
'allowed_mount_roots': [],
|
||||
'default_workspace': '',
|
||||
},
|
||||
'e2b': {'api_key': '', 'api_url': '', 'template': ''},
|
||||
}
|
||||
}
|
||||
),
|
||||
@@ -26,7 +30,7 @@ def make_app(logger: Mock, runtime_url: str = ''):
|
||||
|
||||
|
||||
def test_box_runtime_connector_stdio_when_no_url(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Without runtime_url, on a non-Docker Unix platform, use stdio."""
|
||||
"""Without runtime.endpoint, on a non-Docker Unix platform, use stdio."""
|
||||
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'linux')
|
||||
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', False)
|
||||
connector = BoxRuntimeConnector(make_app(Mock()))
|
||||
@@ -36,11 +40,11 @@ def test_box_runtime_connector_stdio_when_no_url(monkeypatch: pytest.MonkeyPatch
|
||||
|
||||
|
||||
def test_box_runtime_connector_ws_when_url_configured(monkeypatch: pytest.MonkeyPatch):
|
||||
"""With an explicit runtime_url, always use WebSocket."""
|
||||
"""With an explicit runtime.endpoint, always use WebSocket."""
|
||||
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'linux')
|
||||
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', False)
|
||||
logger = Mock()
|
||||
connector = BoxRuntimeConnector(make_app(logger, runtime_url='http://box-runtime:5410'))
|
||||
connector = BoxRuntimeConnector(make_app(logger, runtime_endpoint='http://box-runtime:5410'))
|
||||
|
||||
assert connector._uses_websocket() is True
|
||||
assert isinstance(connector.client, ActionRPCBoxClient)
|
||||
@@ -76,7 +80,7 @@ def test_box_runtime_connector_ws_relay_url_default(monkeypatch: pytest.MonkeyPa
|
||||
def test_box_runtime_connector_ws_relay_url_explicit(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr('langbot.pkg.utils.platform.get_platform', lambda: 'linux')
|
||||
monkeypatch.setattr('langbot.pkg.utils.platform.standalone_box', False)
|
||||
connector = BoxRuntimeConnector(make_app(Mock(), runtime_url='http://box-runtime:5410'))
|
||||
connector = BoxRuntimeConnector(make_app(Mock(), runtime_endpoint='http://box-runtime:5410'))
|
||||
assert connector.ws_relay_base_url == 'http://box-runtime:5410'
|
||||
|
||||
|
||||
|
||||
@@ -67,12 +67,15 @@ class _InProcessBoxRuntimeClient(BoxRuntimeClient):
|
||||
async def start_managed_process(self, session_id: str, spec: BoxManagedProcessSpec):
|
||||
return await self._runtime.start_managed_process(session_id, spec)
|
||||
|
||||
async def get_managed_process(self, session_id: str):
|
||||
return self._runtime.get_managed_process(session_id)
|
||||
async def get_managed_process(self, session_id: str, process_id: str = 'default'):
|
||||
return self._runtime.get_managed_process(session_id, process_id)
|
||||
|
||||
async def get_session(self, session_id: str):
|
||||
return self._runtime.get_session(session_id)
|
||||
|
||||
async def init(self, config: dict) -> None:
|
||||
self._runtime.init(config)
|
||||
|
||||
|
||||
class FakeBackend(BaseSandboxBackend):
|
||||
def __init__(self, logger: Mock, available: bool = True):
|
||||
@@ -141,19 +144,24 @@ def make_query(query_id: int = 42) -> pipeline_query.Query:
|
||||
|
||||
def make_app(
|
||||
logger: Mock,
|
||||
allowed_host_mount_roots: list[str] | None = None,
|
||||
allowed_mount_roots: list[str] | None = None,
|
||||
profile: str = 'default',
|
||||
shared_host_root: str = '',
|
||||
host_root: str = '',
|
||||
workspace_quota_mb: int | None = None,
|
||||
):
|
||||
box_config = {
|
||||
'profile': profile,
|
||||
'shared_host_root': shared_host_root,
|
||||
'allowed_host_mount_roots': allowed_host_mount_roots or [],
|
||||
'default_host_workspace': '',
|
||||
'backend': 'local',
|
||||
'runtime': {'endpoint': ''},
|
||||
'local': {
|
||||
'profile': profile,
|
||||
'host_root': host_root,
|
||||
'allowed_mount_roots': allowed_mount_roots or [],
|
||||
'default_workspace': '',
|
||||
},
|
||||
'e2b': {'api_key': '', 'api_url': '', 'template': ''},
|
||||
}
|
||||
if workspace_quota_mb is not None:
|
||||
box_config['workspace_quota_mb'] = workspace_quota_mb
|
||||
box_config['local']['workspace_quota_mb'] = workspace_quota_mb
|
||||
|
||||
return SimpleNamespace(
|
||||
logger=logger,
|
||||
@@ -293,14 +301,14 @@ async def test_box_service_allows_host_mount_under_configured_root(tmp_path):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_box_service_uses_default_host_workspace_when_host_path_omitted(tmp_path):
|
||||
async def test_box_service_uses_default_workspace_when_host_path_omitted(tmp_path):
|
||||
logger = Mock()
|
||||
backend = FakeBackend(logger)
|
||||
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
|
||||
host_dir = tmp_path / 'default-workspace'
|
||||
host_dir.mkdir()
|
||||
app = make_app(logger, [str(tmp_path)])
|
||||
app.instance_config.data['box']['default_host_workspace'] = str(host_dir)
|
||||
app.instance_config.data['box']['local']['default_workspace'] = str(host_dir)
|
||||
service = BoxService(app, client=_InProcessBoxRuntimeClient(logger, runtime))
|
||||
await service.initialize()
|
||||
|
||||
@@ -313,36 +321,36 @@ async def test_box_service_uses_default_host_workspace_when_host_path_omitted(tm
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_box_service_creates_default_host_workspace_on_initialize(tmp_path):
|
||||
async def test_box_service_creates_default_workspace_on_initialize(tmp_path):
|
||||
logger = Mock()
|
||||
backend = FakeBackend(logger)
|
||||
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
|
||||
allowed_root = tmp_path / 'allowed-root'
|
||||
allowed_root.mkdir()
|
||||
default_host_workspace = allowed_root / 'default-workspace'
|
||||
default_workspace = allowed_root / 'default-workspace'
|
||||
app = make_app(logger, [str(allowed_root)])
|
||||
app.instance_config.data['box']['default_host_workspace'] = str(default_host_workspace)
|
||||
app.instance_config.data['box']['local']['default_workspace'] = str(default_workspace)
|
||||
service = BoxService(app, client=_InProcessBoxRuntimeClient(logger, runtime))
|
||||
|
||||
await service.initialize()
|
||||
|
||||
assert default_host_workspace.is_dir()
|
||||
assert default_workspace.is_dir()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_box_service_derives_workspace_and_allowed_root_from_shared_host_root(tmp_path):
|
||||
async def test_box_service_derives_workspace_and_allowed_root_from_host_root(tmp_path):
|
||||
logger = Mock()
|
||||
backend = FakeBackend(logger)
|
||||
runtime = BoxRuntime(logger=logger, backends=[backend], session_ttl_sec=300)
|
||||
shared_root = tmp_path / 'shared-box-root'
|
||||
app = make_app(logger, shared_host_root=str(shared_root))
|
||||
app = make_app(logger, host_root=str(shared_root))
|
||||
service = BoxService(app, client=_InProcessBoxRuntimeClient(logger, runtime))
|
||||
|
||||
await service.initialize()
|
||||
|
||||
assert service.shared_host_root == os.path.realpath(shared_root)
|
||||
assert service.default_host_workspace == os.path.realpath(shared_root / 'default')
|
||||
assert service.allowed_host_mount_roots == [os.path.realpath(shared_root)]
|
||||
assert service.host_root == os.path.realpath(shared_root)
|
||||
assert service.default_workspace == os.path.realpath(shared_root / 'default')
|
||||
assert service.allowed_mount_roots == [os.path.realpath(shared_root)]
|
||||
assert (shared_root / 'default').is_dir()
|
||||
|
||||
|
||||
@@ -557,9 +565,10 @@ async def test_profile_default_provides_defaults():
|
||||
|
||||
assert result['ok'] is True
|
||||
spec = backend.start_specs[0]
|
||||
profile = BUILTIN_PROFILES['default']
|
||||
assert spec.network == BoxNetworkMode.OFF
|
||||
assert spec.image == 'python:3.11-slim'
|
||||
assert spec.timeout_sec == 30
|
||||
assert spec.image == profile.image
|
||||
assert spec.timeout_sec == profile.timeout_sec
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -698,7 +707,7 @@ async def test_box_service_applies_workspace_quota_from_config(tmp_path):
|
||||
host_dir = tmp_path / 'default-workspace'
|
||||
host_dir.mkdir()
|
||||
app = make_app(logger, [str(tmp_path)], workspace_quota_mb=32)
|
||||
app.instance_config.data['box']['default_host_workspace'] = str(host_dir)
|
||||
app.instance_config.data['box']['local']['default_workspace'] = str(host_dir)
|
||||
service = BoxService(app, client=_InProcessBoxRuntimeClient(logger, runtime))
|
||||
|
||||
await service.initialize()
|
||||
@@ -716,7 +725,7 @@ async def test_box_service_rejects_execution_when_workspace_already_exceeds_quot
|
||||
host_dir.mkdir()
|
||||
(host_dir / 'already-too-large.bin').write_bytes(b'x' * (2 * 1024 * 1024))
|
||||
app = make_app(logger, [str(tmp_path)], workspace_quota_mb=1)
|
||||
app.instance_config.data['box']['default_host_workspace'] = str(host_dir)
|
||||
app.instance_config.data['box']['local']['default_workspace'] = str(host_dir)
|
||||
service = BoxService(app, client=_InProcessBoxRuntimeClient(logger, runtime))
|
||||
|
||||
await service.initialize()
|
||||
@@ -735,7 +744,7 @@ async def test_box_service_rejects_and_cleans_up_when_execution_exceeds_workspac
|
||||
host_dir = tmp_path / 'quota-workspace-post'
|
||||
host_dir.mkdir()
|
||||
app = make_app(logger, [str(tmp_path)], workspace_quota_mb=1)
|
||||
app.instance_config.data['box']['default_host_workspace'] = str(host_dir)
|
||||
app.instance_config.data['box']['local']['default_workspace'] = str(host_dir)
|
||||
service = BoxService(app, client=_InProcessBoxRuntimeClient(logger, runtime))
|
||||
|
||||
await service.initialize()
|
||||
|
||||
@@ -79,6 +79,7 @@ async def test_workspace_session_execute_for_query_uses_session_payload():
|
||||
'session_id': 'skill-person_123-demo',
|
||||
'workdir': '/workspace',
|
||||
'env': {'FOO': 'bar'},
|
||||
'persistent': False,
|
||||
'host_path': '/tmp/project',
|
||||
'host_path_mode': 'rw',
|
||||
'cmd': 'python run.py',
|
||||
@@ -111,6 +112,7 @@ async def test_workspace_session_start_managed_process_rewrites_command_and_args
|
||||
'args': ['/workspace/server.py', '--config', '/workspace/config.json'],
|
||||
'env': {'TOKEN': '1'},
|
||||
'cwd': '/workspace',
|
||||
'process_id': 'default',
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +135,7 @@ def test_workspace_session_build_session_payload_keeps_generic_workspace_shape()
|
||||
'session_id': 'workspace-1',
|
||||
'workdir': '/workspace',
|
||||
'env': {'FOO': 'bar'},
|
||||
'persistent': False,
|
||||
'network': 'on',
|
||||
'read_only_rootfs': False,
|
||||
'host_path': '/tmp/project',
|
||||
|
||||
@@ -528,7 +528,7 @@ class TestGetRuntimeInfoDict:
|
||||
ap=ap,
|
||||
)
|
||||
info = s.get_runtime_info_dict()
|
||||
assert info['box_session_id'] == 'mcp-test-uuid'
|
||||
assert info['box_session_id'] == 'mcp-shared'
|
||||
assert info['box_enabled'] is True
|
||||
|
||||
def test_stdio_session_without_box_runtime(self, mcp_module):
|
||||
|
||||
@@ -92,12 +92,7 @@ class TestSkillManagerActivation:
|
||||
'beta': _make_skill_data(name='beta'),
|
||||
}
|
||||
|
||||
response = (
|
||||
'[ACTIVATE_SKILL: alpha]\n'
|
||||
'[ACTIVATE_SKILL: beta]\n'
|
||||
'[ACTIVATE_SKILL: alpha]\n'
|
||||
'Let me handle this.'
|
||||
)
|
||||
response = '[ACTIVATE_SKILL: alpha]\n[ACTIVATE_SKILL: beta]\n[ACTIVATE_SKILL: alpha]\nLet me handle this.'
|
||||
|
||||
assert mgr.detect_skill_activations(response) == ['alpha', 'beta']
|
||||
assert mgr.detect_skill_activation(response) == 'alpha'
|
||||
@@ -240,7 +235,9 @@ class TestSkillAuthoringToolLoader:
|
||||
|
||||
ap = _make_ap()
|
||||
ap.skill_service = SimpleNamespace(
|
||||
create_skill=AsyncMock(return_value=_make_skill_data(name='prompt-skill', package_root='/data/skills/prompt-skill')),
|
||||
create_skill=AsyncMock(
|
||||
return_value=_make_skill_data(name='prompt-skill', package_root='/data/skills/prompt-skill')
|
||||
),
|
||||
reload_skills=AsyncMock(),
|
||||
list_skills=AsyncMock(return_value=[]),
|
||||
)
|
||||
@@ -329,7 +326,9 @@ class TestSkillAuthoringToolLoader:
|
||||
ap = _make_ap()
|
||||
ap.skill_service = SimpleNamespace(
|
||||
create_skill=AsyncMock(),
|
||||
update_skill=AsyncMock(return_value=_make_skill_data(name='time-now', package_root='/data/skills/time-now')),
|
||||
update_skill=AsyncMock(
|
||||
return_value=_make_skill_data(name='time-now', package_root='/data/skills/time-now')
|
||||
),
|
||||
reload_skills=AsyncMock(),
|
||||
list_skills=AsyncMock(return_value=[]),
|
||||
)
|
||||
@@ -393,7 +392,7 @@ class TestSkillAuthoringToolLoader:
|
||||
)
|
||||
|
||||
ap = _make_ap()
|
||||
ap.box_service = SimpleNamespace(default_host_workspace='/tmp/langbot-workspace')
|
||||
ap.box_service = SimpleNamespace(default_workspace='/tmp/langbot-workspace')
|
||||
ap.skill_service = SimpleNamespace(
|
||||
scan_directory=Mock(
|
||||
return_value={
|
||||
@@ -413,7 +412,7 @@ class TestSkillAuthoringToolLoader:
|
||||
await loader.initialize()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
ap.box_service.default_host_workspace = tmpdir
|
||||
ap.box_service.default_workspace = tmpdir
|
||||
repo_dir = os.path.join(tmpdir, 'repos', 'cloned-skill')
|
||||
os.makedirs(repo_dir)
|
||||
|
||||
@@ -445,7 +444,7 @@ class TestSkillAuthoringToolLoader:
|
||||
)
|
||||
|
||||
ap = _make_ap()
|
||||
ap.box_service = SimpleNamespace(default_host_workspace='/tmp/langbot-workspace')
|
||||
ap.box_service = SimpleNamespace(default_workspace='/tmp/langbot-workspace')
|
||||
ap.skill_service = SimpleNamespace(
|
||||
scan_directory=Mock(),
|
||||
create_skill=AsyncMock(),
|
||||
@@ -501,7 +500,7 @@ class TestNativeToolLoaderSkillPaths:
|
||||
f.write('demo instructions')
|
||||
|
||||
ap = _make_ap()
|
||||
ap.box_service = SimpleNamespace(available=True, default_host_workspace=tmpdir)
|
||||
ap.box_service = SimpleNamespace(available=True, default_workspace=tmpdir)
|
||||
ap.skill_mgr = SimpleNamespace(skills={'demo': _make_skill_data(name='demo', package_root=tmpdir)})
|
||||
loader = NativeToolLoader(ap)
|
||||
|
||||
@@ -522,7 +521,7 @@ class TestNativeToolLoaderSkillPaths:
|
||||
ap = _make_ap()
|
||||
ap.box_service = SimpleNamespace(
|
||||
available=True,
|
||||
default_host_workspace=tmpdir,
|
||||
default_workspace=tmpdir,
|
||||
execute_spec_payload=AsyncMock(return_value={'ok': True}),
|
||||
)
|
||||
ap.skill_mgr = SimpleNamespace(refresh_skill_from_disk=Mock())
|
||||
@@ -555,7 +554,7 @@ class TestNativeToolLoaderSkillPaths:
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
ap = _make_ap()
|
||||
ap.box_service = SimpleNamespace(available=True, default_host_workspace=tmpdir)
|
||||
ap.box_service = SimpleNamespace(available=True, default_workspace=tmpdir)
|
||||
ap.skill_mgr = SimpleNamespace(skills={'demo': _make_skill_data(name='demo', package_root=tmpdir)})
|
||||
loader = NativeToolLoader(ap)
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ async def test_native_tool_loader_exposes_all_tools_when_box_available():
|
||||
|
||||
def _make_loader_with_workspace(tmpdir: str) -> tuple[NativeToolLoader, Mock]:
|
||||
logger = Mock()
|
||||
box_service = SimpleNamespace(available=True, default_host_workspace=tmpdir)
|
||||
box_service = SimpleNamespace(available=True, default_workspace=tmpdir)
|
||||
ap = SimpleNamespace(box_service=box_service, logger=logger)
|
||||
return NativeToolLoader(ap), logger
|
||||
|
||||
|
||||
Reference in New Issue
Block a user