mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-23 00:45:25 +08:00
fix(pipeline): decouple Runner from extension permissions (LANG-54) (#2723)
* fix(pipeline): decouple runner from extension allowlist Co-authored-by: multica-agent <[email protected]> * fix(pipeline): complete runner diagnostics and real runtime coverage Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
5a6ddbff6f
commit
d9e4e7d9df
@@ -0,0 +1,124 @@
|
||||
"""Runner selection must survive restricted extension preferences on disk."""
|
||||
|
||||
from types import SimpleNamespace as NS
|
||||
import json
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
|
||||
from langbot.pkg.api.http.service.pipeline import PipelineService
|
||||
from langbot.pkg.entity.persistence.pipeline import LegacyPipeline
|
||||
from langbot.pkg.workspace.errors import WorkspaceNotFoundError
|
||||
from tests.unit_tests.api.service.test_pipeline_migration import env, context, RID # noqa: F401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_and_mcp_share_real_persistence_without_expanding_extensions(env, monkeypatch): # noqa: F811
|
||||
from quart import Quart
|
||||
from langbot.pkg.api.http.controller.groups.pipelines.pipelines import PipelinesRouterGroup
|
||||
from langbot.pkg.api.mcp.server import LangBotMCPServer
|
||||
|
||||
monkeypatch.setattr('langbot.pkg.api.http.service.workspace_limits.workspace_limits', AsyncMock(return_value={}))
|
||||
monkeypatch.setattr('langbot.pkg.api.mcp.server.get_request_context', context)
|
||||
monkeypatch.setattr('langbot.pkg.api.mcp.server.get_workspace_language', AsyncMock(return_value='en_US'))
|
||||
monkeypatch.setattr('langbot.pkg.api.http.controller.group.get_workspace_language', AsyncMock(return_value='en_US'))
|
||||
env.ap.ver_mgr = NS(get_current_version=Mock(return_value='4.10'))
|
||||
env.ap.pipeline_mgr.load_pipeline = AsyncMock()
|
||||
env.ap.pipeline_mgr.remove_pipeline = AsyncMock()
|
||||
env.ap.pipeline_service = PipelineService(env.ap)
|
||||
app = Quart(__name__)
|
||||
routes = PipelinesRouterGroup(env.ap, app)
|
||||
monkeypatch.setattr(routes, '_authenticate_api_key', AsyncMock(return_value=context()))
|
||||
monkeypatch.setattr(routes, '_record_operation', AsyncMock())
|
||||
await routes.initialize()
|
||||
http = app.test_client()
|
||||
mcp = LangBotMCPServer(env.ap)
|
||||
headers = {'X-API-Key': 'isolated-test-key'}
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'id': RID, 'expire-time': 0},
|
||||
'runner_config': {RID: {'unknown': {'keep': [None, False, ' raw ']}}},
|
||||
}
|
||||
}
|
||||
prefs = {'enable_all_plugins': False, 'plugins': [], 'enable_all_mcp_servers': False, 'mcp_servers': []}
|
||||
async with env.engine.begin() as conn:
|
||||
await conn.execute(
|
||||
sa.update(LegacyPipeline)
|
||||
.where(LegacyPipeline.uuid == 'one')
|
||||
.values(config=config, extensions_preferences=prefs)
|
||||
)
|
||||
response = await http.put('/api/v1/pipelines/one', headers=headers, json={'config': config})
|
||||
assert response.status_code == 200, await response.get_data(as_text=True)
|
||||
assert (await response.get_json())['code'] == 0
|
||||
result = await mcp.mcp.call_tool('get_pipeline', {'pipeline_uuid': 'one'})
|
||||
saved = json.loads(result[0][0].text)
|
||||
assert saved['config'] == config
|
||||
assert saved['extensions_preferences'] == prefs
|
||||
config['ai']['runner_config'][RID]['edited'] = 'via MCP'
|
||||
result = await mcp.mcp.call_tool('update_pipeline', {'pipeline_uuid': 'one', 'pipeline_data': {'config': config}})
|
||||
assert json.loads(result[0][0].text) == {'ok': True}
|
||||
copied_response = await http.post('/api/v1/pipelines/one/copy', headers=headers)
|
||||
assert copied_response.status_code == 200, await copied_response.get_data(as_text=True)
|
||||
copied = (await copied_response.get_json())['data']['uuid']
|
||||
for pipeline_id in ('one', copied):
|
||||
response = await http.get(f'/api/v1/pipelines/{pipeline_id}', headers=headers)
|
||||
saved = (await response.get_json())['data']['pipeline']
|
||||
assert saved['config'] == config
|
||||
assert saved['extensions_preferences']['plugins'] == []
|
||||
assert saved['extensions_preferences']['enable_all_plugins'] is False
|
||||
async with env.engine.connect() as conn:
|
||||
row = (
|
||||
await conn.execute(
|
||||
sa.select(LegacyPipeline.config, LegacyPipeline.extensions_preferences).where(
|
||||
LegacyPipeline.uuid == pipeline_id
|
||||
)
|
||||
)
|
||||
).one()
|
||||
assert row.config == config
|
||||
assert row.extensions_preferences['plugins'] == []
|
||||
assert row.extensions_preferences['enable_all_plugins'] is False
|
||||
body = {'name': 'HTTP creation', 'description': '', 'config': config, 'extensions_preferences': prefs}
|
||||
response = await http.post('/api/v1/pipelines', headers=headers, json=body)
|
||||
assert response.status_code == 200, await response.get_data(as_text=True)
|
||||
http_id = (await response.get_json())['data']['uuid']
|
||||
body['name'] = 'MCP creation'
|
||||
result = await mcp.mcp.call_tool('create_pipeline', {'pipeline_data': body})
|
||||
mcp_id = json.loads(result[0][0].text)['uuid']
|
||||
for pipeline_id in (http_id, mcp_id):
|
||||
saved = await env.ap.pipeline_service.get_pipeline(context(), pipeline_id, include_secret=True)
|
||||
assert saved['config'] == config
|
||||
assert saved['extensions_preferences'] == prefs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_and_copy_do_not_expand_extension_permissions(env, monkeypatch): # noqa: F811
|
||||
monkeypatch.setattr(
|
||||
'langbot.pkg.api.http.service.workspace_limits.workspace_limits',
|
||||
AsyncMock(return_value={}),
|
||||
)
|
||||
env.ap.ver_mgr = NS(get_current_version=Mock(return_value='4.10'))
|
||||
env.ap.pipeline_mgr.load_pipeline = AsyncMock()
|
||||
env.ap.pipeline_mgr.remove_pipeline = AsyncMock()
|
||||
service = PipelineService(env.ap)
|
||||
config = {'ai': {'runner': {'id': RID}, 'runner_config': {RID: {}}}}
|
||||
prefs = {'enable_all_plugins': False, 'plugins': [], 'enable_all_mcp_servers': False, 'mcp_servers': []}
|
||||
# Existing, pre-fix configuration: selected Runner with no extensions.
|
||||
async with env.engine.begin() as conn:
|
||||
await conn.execute(
|
||||
sa.update(LegacyPipeline)
|
||||
.where(LegacyPipeline.uuid == 'one')
|
||||
.values(
|
||||
config=config,
|
||||
extensions_preferences=prefs,
|
||||
)
|
||||
)
|
||||
await service.update_pipeline(context(), 'one', {'config': config, 'name': 'Edited'})
|
||||
copied = await service.copy_pipeline(context(), 'one')
|
||||
for pid in ('one', copied):
|
||||
saved = await service.get_pipeline(context(), pid, include_secret=True)
|
||||
assert saved['config'] == config
|
||||
assert saved['extensions_preferences']['plugins'] == []
|
||||
assert saved['extensions_preferences']['enable_all_plugins'] is False
|
||||
with pytest.raises(WorkspaceNotFoundError):
|
||||
await service.copy_pipeline(context(), 'foreign')
|
||||
Reference in New Issue
Block a user