fix(plugin): share the runtime file-transfer root and 404 uninstalled icon requests

The SDK binds one transfer root inode (mode 0700) and picks it per profile: a shared worker uses /tmp/lbp-rpc while a host without the profile env fell back to data/temp/lbp, so plugin icons and assets died with 'Invalid file transfer capability'. The host now pins the shared root, and a plugin absent from the resolved Workspace answers 404 instead of 500.
This commit is contained in:
Hyu
2026-10-02 01:34:47 +08:00
parent 38d5750785
commit 34b5fe0202
2 changed files with 70 additions and 1 deletions
@@ -165,6 +165,16 @@ except Exception:
pass
def _is_plugin_not_installed(exc: BaseException) -> bool:
"""True when the connector reports a plugin absent from the resolved Workspace.
The connector raises a plain ``ValueError`` for this case, so resource routes
answer 404 instead of surfacing a 500 the WebUI would have to swallow.
"""
return 'is not installed in this workspace' in str(exc).lower()
def _normalize_plugin_asset_path(filepath: str) -> str | None:
filepath = filepath.replace('\\', '/')
if filepath.startswith('/'):
@@ -596,7 +606,15 @@ class PluginsRouterGroup(group.RouterGroup):
request_context: RequestContext,
) -> quart.Response:
await self._require_authenticated_plugin_runtime_context(request_context)
icon_data = await self.ap.plugin_connector.get_plugin_icon(author, plugin_name)
try:
icon_data = await self.ap.plugin_connector.get_plugin_icon(author, plugin_name)
except ValueError as exc:
# A plugin that is not installed for this Workspace is a missing
# resource, not a server fault: the WebUI asks for icons by plugin
# identity and must be able to fall back silently.
if _is_plugin_not_installed(exc):
return quart.Response('Icon not found', status=404)
raise
icon_bytes = await asyncio.to_thread(base64.b64decode, icon_data['plugin_icon_base64'])
return quart.Response(icon_bytes, mimetype=icon_data['mime_type'])
@@ -0,0 +1,51 @@
"""The host must share the Runtime's plugin file-transfer root in shared deployments.
A mismatch leaves plugin icons and assets failing with
"Invalid file transfer capability" because the SDK binds one directory inode per
process (mode 0700) and picks the default from the runtime profile.
"""
from __future__ import annotations
import os
from importlib import import_module
from types import SimpleNamespace
from langbot_plugin.runtime.io.handler import SHARED_WORKER_FILE_STORAGE_DIR
from langbot_plugin.runtime.security import PLUGIN_FILE_STORAGE_DIR_ENV
def connector_module():
import_module('langbot.pkg.core.app')
return import_module('langbot.pkg.plugin.connector')
def align(profile: str) -> None:
connector = connector_module()
connector.PluginRuntimeConnector._align_plugin_file_transfer_root(
SimpleNamespace(runtime_profile=profile)
)
def test_shared_profile_pins_the_runtime_transfer_root(monkeypatch):
monkeypatch.delenv(PLUGIN_FILE_STORAGE_DIR_ENV, raising=False)
align('shared')
assert os.environ[PLUGIN_FILE_STORAGE_DIR_ENV] == SHARED_WORKER_FILE_STORAGE_DIR
def test_explicit_operator_value_is_never_overridden(monkeypatch):
monkeypatch.setenv(PLUGIN_FILE_STORAGE_DIR_ENV, '/var/lib/custom-transfer')
align('shared')
assert os.environ[PLUGIN_FILE_STORAGE_DIR_ENV] == '/var/lib/custom-transfer'
def test_oss_profile_leaves_the_default_alone(monkeypatch):
monkeypatch.delenv(PLUGIN_FILE_STORAGE_DIR_ENV, raising=False)
align('oss_dev')
assert PLUGIN_FILE_STORAGE_DIR_ENV not in os.environ