mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-17 07:50:59 +00:00
fix(box): downgrade get_status.available when backend probed unavailable
Until now ``BoxService.get_status`` returned ``available: true`` whenever
the runtime connector was healthy, even if the runtime itself reported
``backend: { available: false }`` (operator selected nsjail without the
binary, Docker daemon crashed mid-session, E2B credentials wrong, ...).
The dashboard / ``useBoxStatus`` hook / skill_service gate consumed the
top-level flag and showed "connected" while every actual call to native
exec or skill management would fail.
The native-tool loader already polled ``status.backend.available``
independently and hid its tools correctly, but every other consumer
(dashboard banner, the disabled-state hint, the LLM-facing message)
disagreed with it.
Combine the two in the payload: ``available = self._available AND
status.backend.available``. When ``backend.available`` is false we now
also surface a ``connector_error`` that names the backend
("Configured sandbox backend \"nsjail\" is unavailable") so the dialog
shows the actionable reason instead of an empty error pane. The
detailed ``backend`` object is preserved unchanged for the dialog.
Internal ``box_service.available`` (used by ``skill_service`` writes,
``mcp_stdio.uses_box_stdio``, the reconnect callback) is intentionally
NOT changed — it still tracks connector health only, so a backend blip
does not trigger spurious reconnect loops.
Tests:
- ``test_get_status_downgrades_available_when_backend_dead`` — exercise
the new branch (connector OK, backend.available=false → top-level
available=false, connector_error mentions the backend name)
- ``test_get_status_keeps_available_true_when_backend_ok`` — guard
against regressing the happy path
Live-verified with ``box.backend: nsjail`` on macOS (no nsjail binary):
``GET /api/v1/box/status`` now returns ``available: false`` with the
named connector_error, instead of the previous misleading
``available: true``.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -768,10 +768,27 @@ class BoxService:
|
|||||||
'recent_error_count': len(self._recent_errors),
|
'recent_error_count': len(self._recent_errors),
|
||||||
'connector_error': str(exc),
|
'connector_error': str(exc),
|
||||||
}
|
}
|
||||||
return {
|
# Backend state can be unavailable even when the connector is healthy
|
||||||
|
# (operator selected nsjail but the binary is missing, Docker daemon
|
||||||
|
# went down after the runtime started, E2B credentials wrong, ...).
|
||||||
|
# Report the combined state in the top-level ``available`` so the
|
||||||
|
# frontend banner / ``useBoxStatus`` hook / native-tool gate all
|
||||||
|
# agree on "actually usable" rather than "connector alive". The
|
||||||
|
# detailed ``backend`` object stays in the payload so the dialog
|
||||||
|
# can still show which backend was tried.
|
||||||
|
backend_info = runtime_status.get('backend') if isinstance(runtime_status, dict) else None
|
||||||
|
backend_ok = bool(backend_info and backend_info.get('available', False))
|
||||||
|
payload = {
|
||||||
**runtime_status,
|
**runtime_status,
|
||||||
'available': True,
|
'available': backend_ok,
|
||||||
'enabled': self._enabled,
|
'enabled': self._enabled,
|
||||||
'profile': self.profile.name,
|
'profile': self.profile.name,
|
||||||
'recent_error_count': len(self._recent_errors),
|
'recent_error_count': len(self._recent_errors),
|
||||||
}
|
}
|
||||||
|
if not backend_ok and 'connector_error' not in payload:
|
||||||
|
backend_name = backend_info.get('name') if backend_info else None
|
||||||
|
if backend_name:
|
||||||
|
payload['connector_error'] = f'Configured sandbox backend "{backend_name}" is unavailable'
|
||||||
|
else:
|
||||||
|
payload['connector_error'] = 'No supported sandbox backend (Docker / nsjail / E2B) is available'
|
||||||
|
return payload
|
||||||
|
|||||||
@@ -1274,6 +1274,52 @@ class TestBoxDisabledByConfig:
|
|||||||
assert status['enabled'] is True
|
assert status['enabled'] is True
|
||||||
assert 'docker daemon' in status['connector_error']
|
assert 'docker daemon' in status['connector_error']
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_status_downgrades_available_when_backend_dead(self):
|
||||||
|
"""The connector can be healthy while the runtime reports no usable
|
||||||
|
backend (operator selected nsjail but binary missing, Docker daemon
|
||||||
|
crashed after handshake, ...). The top-level ``available`` must
|
||||||
|
reflect the combined state so the dashboard / useBoxStatus hook /
|
||||||
|
skill_service gate stay consistent with the native-tool gate."""
|
||||||
|
logger = Mock()
|
||||||
|
client = Mock(spec=BoxRuntimeClient)
|
||||||
|
client.initialize = AsyncMock()
|
||||||
|
client.get_status = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
'backend': {'name': 'nsjail', 'available': False},
|
||||||
|
'active_sessions': 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
service = BoxService(make_app(logger, enabled=True), client=client)
|
||||||
|
await service.initialize()
|
||||||
|
|
||||||
|
status = await service.get_status()
|
||||||
|
assert status['available'] is False
|
||||||
|
assert status['enabled'] is True
|
||||||
|
# The detailed backend object is preserved for the dialog
|
||||||
|
assert status['backend'] == {'name': 'nsjail', 'available': False}
|
||||||
|
assert 'nsjail' in status['connector_error']
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_status_keeps_available_true_when_backend_ok(self):
|
||||||
|
logger = Mock()
|
||||||
|
client = Mock(spec=BoxRuntimeClient)
|
||||||
|
client.initialize = AsyncMock()
|
||||||
|
client.get_status = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
'backend': {'name': 'docker', 'available': True},
|
||||||
|
'active_sessions': 2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
service = BoxService(make_app(logger, enabled=True), client=client)
|
||||||
|
await service.initialize()
|
||||||
|
|
||||||
|
status = await service.get_status()
|
||||||
|
assert status['available'] is True
|
||||||
|
assert status['backend'] == {'name': 'docker', 'available': True}
|
||||||
|
# No spurious connector_error overlay when everything is healthy
|
||||||
|
assert 'connector_error' not in status or not status['connector_error']
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_disconnect_callback_is_no_op_when_disabled(self):
|
async def test_disconnect_callback_is_no_op_when_disabled(self):
|
||||||
logger = Mock()
|
logger = Mock()
|
||||||
|
|||||||
Reference in New Issue
Block a user