mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-21 16:05:19 +08:00
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:
@@ -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
|
||||
Reference in New Issue
Block a user