fix(agent-runner): close sandbox skill authoring flow

This commit is contained in:
huanghuoguoguo
2026-07-15 08:35:52 +08:00
parent d871baab68
commit 730d52bcd0
12 changed files with 313 additions and 31 deletions
@@ -0,0 +1,48 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import pytest
from langbot.pkg.api.http.service.monitoring import MonitoringService
class _MonitoringMessageRow:
"""Match the Row shape returned by Core connections for ORM selects."""
id = 'message-1'
bot_id = 'bot-1'
bot_name = 'QA Bot'
pipeline_id = 'pipeline-1'
pipeline_name = 'QA Pipeline'
session_id = 'person_1'
def __getitem__(self, index: int) -> str:
assert index == 0
return self.id
@pytest.mark.asyncio
async def test_record_tool_call_uses_full_monitoring_message_row_for_context():
message_row = _MonitoringMessageRow()
select_result = Mock(first=Mock(return_value=message_row))
insert_result = Mock()
persistence_mgr = SimpleNamespace(
execute_async=AsyncMock(side_effect=[select_result, insert_result]),
)
service = MonitoringService(SimpleNamespace(persistence_mgr=persistence_mgr))
await service.record_tool_call(
tool_name='exec',
tool_source='native',
duration=12,
message_id='message-1',
)
insert_statement = persistence_mgr.execute_async.await_args_list[1].args[0]
values = insert_statement.compile().params
assert values['bot_id'] == 'bot-1'
assert values['pipeline_id'] == 'pipeline-1'
assert values['session_id'] == 'person_1'
assert values['message_id'] == 'message-1'
@@ -440,6 +440,48 @@ class TestSkillToolLoader:
assert result['skill_name'] == 'cloned-skill'
assert result['source_path'] == '/workspace/repo'
@pytest.mark.asyncio
async def test_registered_skill_can_be_activated_in_same_query(self):
from langbot.pkg.provider.tools.loaders.skill import PIPELINE_BOUND_SKILLS_KEY
from langbot.pkg.provider.tools.loaders.skill_authoring import (
ACTIVATE_SKILL_TOOL_NAME,
REGISTER_SKILL_TOOL_NAME,
SkillToolLoader,
)
with tempfile.TemporaryDirectory() as tmpdir:
repo_dir = os.path.join(tmpdir, 'repo')
os.makedirs(repo_dir)
created = _make_skill_data(name='cloned-skill', package_root=os.path.realpath(repo_dir))
ap = _make_ap()
ap.box_service = SimpleNamespace(default_workspace=tmpdir, available=True)
ap.skill_mgr = SimpleNamespace(skills={'existing': _make_skill_data(name='existing')})
async def create_skill(_data):
ap.skill_mgr.skills['cloned-skill'] = created
return created
ap.skill_service = SimpleNamespace(
scan_directory_async=AsyncMock(return_value=created),
create_skill=AsyncMock(side_effect=create_skill),
)
query = SimpleNamespace(variables={PIPELINE_BOUND_SKILLS_KEY: ['existing']})
loader = SkillToolLoader(ap)
await loader.invoke_tool(
REGISTER_SKILL_TOOL_NAME,
{'path': '/workspace/repo', 'name': 'cloned-skill'},
query,
)
activated = await loader.invoke_tool(
ACTIVATE_SKILL_TOOL_NAME,
{'skill_name': 'cloned-skill'},
query,
)
assert query.variables[PIPELINE_BOUND_SKILLS_KEY] == ['existing', 'cloned-skill']
assert activated['activated'] is True
@pytest.mark.asyncio
async def test_register_skill_rejects_workspace_escape(self):
from langbot.pkg.provider.tools.loaders.skill_authoring import (
@@ -4,7 +4,7 @@ import base64
import os
import tempfile
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
from unittest.mock import AsyncMock, Mock, patch
import pytest
@@ -236,6 +236,32 @@ async def test_write_creates_subdirectories():
assert f.read() == 'nested'
@pytest.mark.asyncio
async def test_write_falls_back_to_box_for_container_owned_workspace_file():
with tempfile.TemporaryDirectory() as tmpdir:
box_service = SimpleNamespace(
available=True,
default_workspace=tmpdir,
execute_tool=AsyncMock(
return_value={
'ok': True,
'stdout': '{"ok": true, "path": "/workspace/root-owned.txt"}',
}
),
)
loader = NativeToolLoader(SimpleNamespace(box_service=box_service, logger=Mock()))
loader._write_host_file = Mock(side_effect=PermissionError)
result = await loader.invoke_tool(
'write',
{'path': '/workspace/root-owned.txt', 'content': 'updated'},
_make_query(),
)
assert result == {'ok': True, 'path': '/workspace/root-owned.txt'}
box_service.execute_tool.assert_awaited_once()
@pytest.mark.asyncio
async def test_read_binary_file_as_base64_chunk():
with tempfile.TemporaryDirectory() as tmpdir:
@@ -326,6 +352,35 @@ async def test_edit_replaces_unique_string():
assert f.read() == 'def foo():\n return 42\n'
@pytest.mark.asyncio
async def test_edit_falls_back_to_box_for_container_owned_workspace_file():
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, 'root-owned.txt')
with open(path, 'w') as f:
f.write('before')
box_service = SimpleNamespace(
available=True,
default_workspace=tmpdir,
execute_tool=AsyncMock(
return_value={
'ok': True,
'stdout': '{"ok": true, "path": "/workspace/root-owned.txt"}',
}
),
)
loader = NativeToolLoader(SimpleNamespace(box_service=box_service, logger=Mock()))
with patch('builtins.open', side_effect=PermissionError):
result = await loader.invoke_tool(
'edit',
{'path': '/workspace/root-owned.txt', 'old_string': 'before', 'new_string': 'after'},
_make_query(),
)
assert result == {'ok': True, 'path': '/workspace/root-owned.txt'}
box_service.execute_tool.assert_awaited_once()
@pytest.mark.asyncio
async def test_edit_rejects_ambiguous_match():
with tempfile.TemporaryDirectory() as tmpdir: