Files
LangBot/tests/integration_tests/box/test_outbox_export.py
T
947d8addea fix(box): validate and export explicitly selected files (#2641)
* fix(box): retain outbox files when export fails

* test(box): retain ownership evidence in CI logs

* test(box): exercise LocalAgent export repair through real runtime and Docker

Co-authored-by: multica-agent <[email protected]>

* fix(box): complete bounded outbox recovery contract

* feat(box): export explicitly selected files

Co-authored-by: multica-agent <[email protected]>

* test(box): exercise explicit export on real Docker

Co-authored-by: multica-agent <[email protected]>

* test(box): verify explicit exports with live model and Docker

Co-authored-by: multica-agent <[email protected]>

* fix(box): align explicit export SDK and integration contracts

* docs(box): describe selected-file export contract

---------

Co-authored-by: HaiYangBG1 <[email protected]>
Co-authored-by: Chin Rock <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-19 15:31:33 +08:00

112 lines
5.3 KiB
Python

"""Real Docker file export through Core and the SDK, with ownership evidence."""
import base64
import json
import logging
import os
import shlex
import sys
from unittest.mock import AsyncMock
import pytest
from langbot.pkg.box.runner import RunnerBoxService, binding_for, exported_message
from langbot.pkg.box.service import BoxService
from langbot_plugin.api.entities.builtin.platform.message import File, MessageChain
from langbot_plugin.box.backend import DockerBackend
from langbot_plugin.box.runtime import BoxRuntime
from tests.unit_tests.box.test_box_service import _InProcessBoxRuntimeClient, make_app, make_query, _CONTEXT
class _TestDockerBackend(DockerBackend):
async def cleanup_orphaned_containers(self, current_instance_id=''):
# Never inspect or remove another developer's Box containers.
pass
class _Client(_InProcessBoxRuntimeClient):
async def create_session(self, spec, *, action_context=None):
return await self._runtime.create_session(spec, action_context=action_context)
async def execute(self, spec, *, action_context=None):
return await self._runtime.execute(spec, action_context=action_context)
async def get_sessions(self, *, action_context=None):
return self._runtime.get_sessions_for_workspace(action_context)
@pytest.mark.asyncio
async def test_root_owned_output_exports_from_reused_box_without_cross_run_leaks(tmp_path, capfd):
logger = logging.getLogger('test.box.outbox')
backend = _TestDockerBackend(logger)
if not await backend.is_available():
pytest.skip('Docker is required for file ownership/export integration')
if sys.platform == 'linux' and os.geteuid() == 0:
pytest.skip('the Linux ownership regression requires non-root Core')
app = make_app(logger, allowed_mount_roots=[str(tmp_path)], host_root=str(tmp_path / 'box'))
app.instance_config.data['box']['backend'] = 'docker'
app.instance_config.data['box']['local']['image'] = 'python:3.12-alpine'
runtime = BoxRuntime(logger=logger, backends=[backend], max_sessions=1)
box = BoxService(app, client=_Client(logger, runtime))
api = RunnerBoxService(box)
await box.initialize()
original_reader = box._read_outbox_via_exec
box._read_outbox_via_exec = AsyncMock(wraps=original_reader)
try:
first_box = await api.acquire(_CONTEXT, {'reuse_key': 'export-regression'})
previous_file_id = None
for index in range(2):
acquired = await api.acquire(_CONTEXT, {'reuse_key': 'export-regression'})
assert acquired['id'] == first_box['id']
query = make_query(index)
object.__setattr__(query, '_box_binding', None)
await api.bind(_CONTEXT, query, f'export-run-{index}', acquired['id'])
payload = f'exact input for run {index}'.encode()
query.message_chain = MessageChain([File(name='input.txt', base64=base64.b64encode(payload).decode())])
imported = await api.import_attachments(query, None)
source = imported['items'][0]['path']
outbox = f'/workspace/outbox/{binding_for(query).io_scope}'
target = outbox + '/answer.txt'
copied = await box.execute_tool(
{'command': f'mkdir -p {shlex.quote(outbox)} && cp {shlex.quote(source)} {shlex.quote(target)}'},
query,
)
assert copied['ok'], copied
stat_script = (
'import json, os, stat; '
f's = os.stat({target!r}); '
'print(json.dumps(dict(uid=s.st_uid, gid=s.st_gid, mode=stat.S_IMODE(s.st_mode))))'
)
evidence = await box.execute_tool({'command': 'python3 -c ' + shlex.quote(stat_script)}, query)
assert evidence['ok'], evidence
metadata = json.loads(evidence['stdout'])
# Keep ownership evidence in CI logs even when the test passes.
with capfd.disabled():
print(f'Core uid={os.geteuid()}; Docker copy: {metadata}', flush=True)
assert metadata['mode'] == 0o600
if sys.platform == 'linux':
# Linux CI exercises the issue's exact root-owned 0600 case.
assert metadata['uid'] == 0
assert metadata['gid'] == 0
# Explicit selections always use the reader inside the bound Box.
exported = await api.export_files(query, [target])
assert len(exported['items']) == 1
item = exported['items'][0]
assert item['size'] == len(payload)
assert item['name'] == 'answer.txt'
message = exported_message(query, [item['id']])
assert base64.b64decode(message.root[0].base64) == payload
if previous_file_id:
from langbot_plugin.box.errors import BoxValidationError
with pytest.raises(BoxValidationError, match='does not belong'):
exported_message(query, [previous_file_id])
previous_file_id = item['id']
remaining = await box.execute_tool({'command': f'find {shlex.quote(outbox)} -type f'}, query)
assert remaining['ok'], remaining
assert remaining['stdout'].strip() == ''
assert len(await api.sessions(_CONTEXT)) == 1
assert box._read_outbox_via_exec.await_count == 2
finally:
await box.shutdown()