feat: delegate sandbox policy to runners and simplify pipeline migration

This commit is contained in:
RockChinQ
2026-09-17 23:05:27 +08:00
parent 8c119bc4b6
commit b264d46d77
57 changed files with 2111 additions and 1751 deletions
+67
View File
@@ -0,0 +1,67 @@
"""Authenticated plugin Box resources and invocation file operations."""
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction as Action
from langbot_plugin.runtime.io.handler import ActionResponse
from ..box.runner import RunnerBoxService
from .agent_run_support import _validate_agent_run_session
def register(h):
async def dispatch(action, data):
from .handler import _resolve_action_query
action_context, _ = await h._require_plugin_action_context()
context = h._execution_context(action_context)
query = None
run_id = data.get('run_id')
context_actions = {Action.BIND_BOX, Action.IMPORT_BOX_ATTACHMENTS, Action.EXPORT_BOX_FILES}
if action in context_actions and not run_id:
return ActionResponse.error('run_id is required for Box context operations')
if run_id:
session, error = await _validate_agent_run_session(
run_id,
data.get('caller_plugin_identity'),
h.ap,
'Box API',
api_capability='box',
)
if error:
return error
query = _resolve_action_query(data, session, h.ap, action_context)
if query is None:
return ActionResponse.error('The current execution context has expired')
service = RunnerBoxService(h.ap.box_service)
if action == Action.GET_BOX_STATUS:
return ActionResponse.success(await service.status(context))
if action == Action.LIST_BOXES:
return ActionResponse.success(
{'items': [service.public_session(s) for s in await service.sessions(context)]}
)
if action == Action.ACQUIRE_BOX:
request = {k: v for k, v in data.items() if k not in {'run_id', 'caller_plugin_identity'}}
return ActionResponse.success(await service.acquire(context, request, query))
if action == Action.BIND_BOX:
return ActionResponse.success(await service.bind(context, query, run_id, data['box_id']))
if action == Action.IMPORT_BOX_ATTACHMENTS:
return ActionResponse.success(await service.import_attachments(query, data.get('attachment_ids')))
if action == Action.EXPORT_BOX_FILES:
return ActionResponse.success(await service.export_files(query))
raise ValueError('Unsupported Box action')
def register_action(action):
@h.action(action)
async def invoke(data):
try:
return await dispatch(action, data)
except Exception as exc:
return ActionResponse.error(f'{type(exc).__name__}: {exc}')
for action in (
Action.GET_BOX_STATUS,
Action.LIST_BOXES,
Action.ACQUIRE_BOX,
Action.BIND_BOX,
Action.IMPORT_BOX_ATTACHMENTS,
Action.EXPORT_BOX_FILES,
):
register_action(action)
+19
View File
@@ -1631,6 +1631,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
plugin_author: str,
plugin_name: str,
task_context: taskmgr.TaskContext | None,
version: str | None = None,
) -> tuple[bytes | None, str | None]:
"""Return a plugin package, or install an MCP/skill and return none."""
@@ -1640,6 +1641,22 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
timeout=15,
event_hooks=httpclient.httpx_response_limit_hooks(_MARKETPLACE_PLUGIN_DOWNLOAD_MAX_BYTES),
) as client:
if version is not None:
if (
not isinstance(version, str)
or not version
or any(
c not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._+-' for c in version
)
):
raise ValueError('Invalid plugin version')
_status, package = await _marketplace_get(
client,
f'{space_url}/api/v1/marketplace/plugins/download/{plugin_author}/{plugin_name}/{version}',
max_bytes=_MARKETPLACE_PLUGIN_DOWNLOAD_MAX_BYTES,
)
return package, version
mcp_status, mcp_body = await _marketplace_get(
client,
f'{space_url}/api/v1/marketplace/mcps/{plugin_author}/{plugin_name}',
@@ -1726,11 +1743,13 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
if task_context is not None:
task_context.set_current_action('downloading plugin package')
task_context.metadata['progress_percent'] = 15
version_options = {'version': install_info['plugin_version']} if install_info.get('plugin_version') else {}
file_bytes, version = await self._download_marketplace_package(
execution_context,
plugin_author,
plugin_name,
task_context,
**version_options,
)
if file_bytes is None:
return
+19
View File
@@ -1188,7 +1188,18 @@ class RuntimeConnectionHandler(handler.Handler):
)
if error:
return error
output_lock = None
output_lock_acquired = False
try:
if data.get('file_ids') is not None:
from ..box.runner import exported_message, binding_for
query = _resolve_action_query(data, session, self.ap, action_context)
output_lock = binding_for(query).lock
await output_lock.acquire()
output_lock_acquired = True
files = exported_message(query, data['file_ids'])
data = {**data, 'params': {**(data.get('params') or {}), 'message': files.model_dump(mode='json')}}
tool_name, parameters, message = resolve_platform_api_call(
session, data.get('bot_uuid'), data['action'], data.get('params') or {}, data.get('context_tool')
)
@@ -1208,9 +1219,14 @@ class RuntimeConnectionHandler(handler.Handler):
parameters,
message_chain=message,
)
if data.get('file_ids') is not None:
exported_message(query, data['file_ids'], consume=True)
return handler.ActionResponse.success(data={'result': _serialize_plugin_api_result(result)})
except (ValueError, KeyError, TypeError) as exc:
return handler.ActionResponse.error(message=str(exc))
finally:
if output_lock_acquired:
output_lock.release()
@self.action(PluginToRuntimeAction.SEND_MESSAGE)
async def send_message(data: dict[str, Any]) -> handler.ActionResponse:
@@ -2634,6 +2650,9 @@ class RuntimeConnectionHandler(handler.Handler):
agent_pull_actions.register(self)
runner_actions.register(self)
from . import box_actions
box_actions.register(self)
agent_state_actions.register(self)
@self.action(CommonAction.PING)