mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 18:06:06 +00:00
feat(agent-runner): enforce 4.x host-owned execution
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from importlib import import_module
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
import quart
|
||||
|
||||
core_app_module = types.ModuleType('langbot.pkg.core.app')
|
||||
core_app_module.Application = object
|
||||
sys.modules.setdefault('langbot.pkg.core.app', core_app_module)
|
||||
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
async def _create_test_client(pipeline_service: SimpleNamespace, **extra_ap):
|
||||
app = quart.Quart(__name__)
|
||||
user_service = SimpleNamespace(
|
||||
verify_jwt_token=AsyncMock(return_value='test@example.com'),
|
||||
get_user_by_email=AsyncMock(return_value=SimpleNamespace(user='test@example.com')),
|
||||
)
|
||||
ap = SimpleNamespace(pipeline_service=pipeline_service, user_service=user_service, **extra_ap)
|
||||
router_class = import_module('langbot.pkg.api.http.controller.groups.pipelines.pipelines').PipelinesRouterGroup
|
||||
group = router_class(ap, app)
|
||||
await group.initialize()
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('method', 'path', 'service_method'),
|
||||
[
|
||||
('post', '/api/v1/pipelines', 'create_pipeline'),
|
||||
('put', '/api/v1/pipelines/pipeline-1', 'update_pipeline'),
|
||||
],
|
||||
)
|
||||
async def test_pipeline_writes_return_bad_request_for_invalid_runner_security_field(
|
||||
method,
|
||||
path,
|
||||
service_method,
|
||||
):
|
||||
message = "Pipeline runner_config['runner-1'] field 'enable-all-tools' must be a boolean"
|
||||
pipeline_service = SimpleNamespace(
|
||||
create_pipeline=AsyncMock(),
|
||||
update_pipeline=AsyncMock(),
|
||||
update_pipeline_extensions=AsyncMock(),
|
||||
)
|
||||
getattr(pipeline_service, service_method).side_effect = ValueError(message)
|
||||
client = await _create_test_client(pipeline_service)
|
||||
|
||||
response = await getattr(client, method)(
|
||||
path,
|
||||
json={'config': {'ai': {'runner': {'id': 'runner-1'}}}},
|
||||
headers={'Authorization': 'Bearer test-token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert await response.get_json() == {'code': -1, 'msg': message}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
||||
async def test_pipeline_extensions_reject_non_boolean_resource_read(invalid_value):
|
||||
pipeline_service = SimpleNamespace(
|
||||
create_pipeline=AsyncMock(),
|
||||
update_pipeline=AsyncMock(),
|
||||
update_pipeline_extensions=AsyncMock(),
|
||||
)
|
||||
client = await _create_test_client(pipeline_service)
|
||||
|
||||
response = await client.put(
|
||||
'/api/v1/pipelines/pipeline-1/extensions',
|
||||
json={'mcp_resource_agent_read_enabled': invalid_value},
|
||||
headers={'Authorization': 'Bearer test-token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert await response.get_json() == {
|
||||
'code': -1,
|
||||
'msg': "Pipeline extension field 'mcp_resource_agent_read_enabled' must be a boolean",
|
||||
}
|
||||
pipeline_service.update_pipeline_extensions.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'field',
|
||||
[
|
||||
'enable_all_plugins',
|
||||
'enable_all_mcp_servers',
|
||||
'enable_all_skills',
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
||||
async def test_pipeline_extensions_reject_non_boolean_enable_all_flags(field, invalid_value):
|
||||
pipeline_service = SimpleNamespace(
|
||||
create_pipeline=AsyncMock(),
|
||||
update_pipeline=AsyncMock(),
|
||||
update_pipeline_extensions=AsyncMock(),
|
||||
)
|
||||
client = await _create_test_client(pipeline_service)
|
||||
|
||||
response = await client.put(
|
||||
'/api/v1/pipelines/pipeline-1/extensions',
|
||||
json={field: invalid_value},
|
||||
headers={'Authorization': 'Bearer test-token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert await response.get_json() == {
|
||||
'code': -1,
|
||||
'msg': f"Pipeline extension field '{field}' must be a boolean",
|
||||
}
|
||||
pipeline_service.update_pipeline_extensions.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('field', 'invalid_value'),
|
||||
[
|
||||
('bound_plugins', 'author/plugin'),
|
||||
('bound_mcp_servers', 'server-1'),
|
||||
('bound_skills', 'skill-1'),
|
||||
('bound_mcp_resources', {'uri': 'file:///README.md'}),
|
||||
],
|
||||
)
|
||||
async def test_pipeline_extensions_return_bad_request_for_malformed_binding_lists(
|
||||
field,
|
||||
invalid_value,
|
||||
):
|
||||
message = f"Pipeline extension field '{field}' must be a list"
|
||||
pipeline_service = SimpleNamespace(
|
||||
create_pipeline=AsyncMock(),
|
||||
update_pipeline=AsyncMock(),
|
||||
update_pipeline_extensions=AsyncMock(side_effect=ValueError(message)),
|
||||
)
|
||||
client = await _create_test_client(pipeline_service)
|
||||
|
||||
response = await client.put(
|
||||
'/api/v1/pipelines/pipeline-1/extensions',
|
||||
json={field: invalid_value},
|
||||
headers={'Authorization': 'Bearer test-token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert await response.get_json() == {'code': -1, 'msg': message}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('invalid_value', [0, None, 'false'])
|
||||
async def test_pipeline_extensions_get_normalizes_malformed_enable_all_flags(invalid_value):
|
||||
pipeline_service = SimpleNamespace(
|
||||
get_pipeline=AsyncMock(
|
||||
return_value={
|
||||
'extensions_preferences': {
|
||||
'enable_all_plugins': invalid_value,
|
||||
'enable_all_mcp_servers': invalid_value,
|
||||
'enable_all_skills': invalid_value,
|
||||
'mcp_resource_agent_read_enabled': invalid_value,
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
client = await _create_test_client(
|
||||
pipeline_service,
|
||||
plugin_connector=SimpleNamespace(list_plugins=AsyncMock(return_value=[])),
|
||||
mcp_service=SimpleNamespace(get_mcp_servers=AsyncMock(return_value=[])),
|
||||
skill_service=SimpleNamespace(list_skills=AsyncMock(return_value=[])),
|
||||
logger=SimpleNamespace(warning=AsyncMock()),
|
||||
)
|
||||
|
||||
response = await client.get(
|
||||
'/api/v1/pipelines/pipeline-1/extensions',
|
||||
headers={'Authorization': 'Bearer test-token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = await response.get_json()
|
||||
assert payload['data']['enable_all_plugins'] is False
|
||||
assert payload['data']['enable_all_mcp_servers'] is False
|
||||
assert payload['data']['enable_all_skills'] is False
|
||||
assert payload['data']['mcp_resource_agent_read_enabled'] is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize('invalid_preferences', [None, [], 'all', 0, False])
|
||||
async def test_pipeline_extensions_get_malformed_root_is_fail_closed(invalid_preferences):
|
||||
pipeline_service = SimpleNamespace(
|
||||
get_pipeline=AsyncMock(
|
||||
return_value={'extensions_preferences': invalid_preferences}
|
||||
),
|
||||
)
|
||||
client = await _create_test_client(
|
||||
pipeline_service,
|
||||
plugin_connector=SimpleNamespace(list_plugins=AsyncMock(return_value=[])),
|
||||
mcp_service=SimpleNamespace(get_mcp_servers=AsyncMock(return_value=[])),
|
||||
skill_service=SimpleNamespace(list_skills=AsyncMock(return_value=[])),
|
||||
logger=SimpleNamespace(warning=AsyncMock()),
|
||||
)
|
||||
|
||||
response = await client.get(
|
||||
'/api/v1/pipelines/pipeline-1/extensions',
|
||||
headers={'Authorization': 'Bearer test-token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = await response.get_json()
|
||||
assert payload['data']['enable_all_plugins'] is False
|
||||
assert payload['data']['enable_all_mcp_servers'] is False
|
||||
assert payload['data']['enable_all_skills'] is False
|
||||
assert payload['data']['mcp_resource_agent_read_enabled'] is False
|
||||
assert payload['data']['bound_plugins'] == []
|
||||
assert payload['data']['bound_mcp_servers'] == []
|
||||
assert payload['data']['bound_skills'] == []
|
||||
assert payload['data']['bound_mcp_resources'] == []
|
||||
Reference in New Issue
Block a user