mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
e631da0073
* fix(runner): align SDK pin and workspace-aware integration fixtures * fix(ci): format sources and resolve current migration head * test(persistence): align standalone migration fixtures with current models * test(web): align smoke fixtures with current processor UI --------- Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
149 lines
4.9 KiB
Python
149 lines
4.9 KiB
Python
"""Validation and fail-closed reads for Pipeline extension preferences."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import typing
|
|
|
|
|
|
_BOOLEAN_FIELDS = (
|
|
'enable_all_plugins',
|
|
'enable_all_mcp_servers',
|
|
'enable_all_skills',
|
|
'mcp_resource_agent_read_enabled',
|
|
)
|
|
|
|
|
|
def _valid_plugin_binding(value: typing.Any) -> bool:
|
|
return (
|
|
isinstance(value, dict)
|
|
and isinstance(value.get('author'), str)
|
|
and bool(value['author'])
|
|
and isinstance(value.get('name'), str)
|
|
and bool(value['name'])
|
|
)
|
|
|
|
|
|
def _valid_name(value: typing.Any) -> bool:
|
|
return isinstance(value, str) and bool(value)
|
|
|
|
|
|
def normalize_extension_preferences(value: typing.Any) -> dict[str, typing.Any]:
|
|
"""Return a safe runtime view of persisted extension preferences.
|
|
|
|
Missing fields in a valid object retain the established defaults. A
|
|
malformed root is treated as an empty allowlist with every extension
|
|
capability disabled.
|
|
"""
|
|
if not isinstance(value, dict):
|
|
return {
|
|
'enable_all_plugins': False,
|
|
'enable_all_mcp_servers': False,
|
|
'enable_all_skills': False,
|
|
'mcp_resource_agent_read_enabled': False,
|
|
'plugins': [],
|
|
'mcp_servers': [],
|
|
'skills': [],
|
|
'mcp_resources': [],
|
|
}
|
|
|
|
normalized = dict(value)
|
|
normalized['enable_all_plugins'] = value.get('enable_all_plugins', True) is True
|
|
normalized['enable_all_mcp_servers'] = value.get('enable_all_mcp_servers', True) is True
|
|
normalized['enable_all_skills'] = value.get('enable_all_skills', True) is True
|
|
normalized['mcp_resource_agent_read_enabled'] = value.get('mcp_resource_agent_read_enabled', True) is True
|
|
|
|
plugins = value.get('plugins', [])
|
|
plugins_are_valid = isinstance(plugins, list) and all(_valid_plugin_binding(plugin) for plugin in plugins)
|
|
normalized['plugins'] = list(plugins) if plugins_are_valid else []
|
|
if not plugins_are_valid:
|
|
normalized['enable_all_plugins'] = False
|
|
|
|
mcp_servers = value.get('mcp_servers', [])
|
|
mcp_servers_are_valid = isinstance(mcp_servers, list) and all(_valid_name(server) for server in mcp_servers)
|
|
normalized['mcp_servers'] = list(mcp_servers) if mcp_servers_are_valid else []
|
|
if not mcp_servers_are_valid:
|
|
normalized['enable_all_mcp_servers'] = False
|
|
|
|
skills = value.get('skills', [])
|
|
skills_are_valid = isinstance(skills, list) and all(_valid_name(skill) for skill in skills)
|
|
normalized['skills'] = list(skills) if skills_are_valid else []
|
|
if not skills_are_valid:
|
|
normalized['enable_all_skills'] = False
|
|
|
|
mcp_resources = value.get('mcp_resources', [])
|
|
mcp_resources_are_valid = isinstance(mcp_resources, list) and all(
|
|
isinstance(resource, dict) for resource in mcp_resources
|
|
)
|
|
normalized['mcp_resources'] = list(mcp_resources) if mcp_resources_are_valid else []
|
|
if not mcp_resources_are_valid:
|
|
normalized['mcp_resource_agent_read_enabled'] = False
|
|
return normalized
|
|
|
|
|
|
def validate_extension_preferences(
|
|
value: typing.Any,
|
|
*,
|
|
context: str = 'Pipeline extensions_preferences',
|
|
field_aliases: typing.Mapping[str, str] | None = None,
|
|
) -> dict[str, typing.Any]:
|
|
"""Reject extension preferences that cannot be consumed unambiguously."""
|
|
if not isinstance(value, dict):
|
|
raise ValueError(f'{context} must be an object')
|
|
|
|
for field_name in _BOOLEAN_FIELDS:
|
|
if field_name in value and not isinstance(value[field_name], bool):
|
|
raise ValueError(f"{context} field '{field_name}' must be a boolean")
|
|
|
|
aliases = field_aliases or {}
|
|
_validate_list_field(
|
|
value,
|
|
'plugins',
|
|
aliases.get('plugins', 'plugins'),
|
|
_valid_plugin_binding,
|
|
'a plugin object with non-empty author and name',
|
|
context,
|
|
)
|
|
_validate_list_field(
|
|
value,
|
|
'mcp_servers',
|
|
aliases.get('mcp_servers', 'mcp_servers'),
|
|
_valid_name,
|
|
'a non-empty string',
|
|
context,
|
|
)
|
|
_validate_list_field(
|
|
value,
|
|
'skills',
|
|
aliases.get('skills', 'skills'),
|
|
_valid_name,
|
|
'a non-empty string',
|
|
context,
|
|
)
|
|
_validate_list_field(
|
|
value,
|
|
'mcp_resources',
|
|
aliases.get('mcp_resources', 'mcp_resources'),
|
|
lambda item: isinstance(item, dict),
|
|
'an object',
|
|
context,
|
|
)
|
|
return value
|
|
|
|
|
|
def _validate_list_field(
|
|
value: dict[str, typing.Any],
|
|
field_name: str,
|
|
field_label: str,
|
|
item_validator: typing.Callable[[typing.Any], bool],
|
|
item_description: str,
|
|
context: str,
|
|
) -> None:
|
|
if field_name not in value:
|
|
return
|
|
items = value[field_name]
|
|
if not isinstance(items, list):
|
|
raise ValueError(f"{context} field '{field_label}' must be a list")
|
|
for index, item in enumerate(items):
|
|
if not item_validator(item):
|
|
raise ValueError(f"{context} field '{field_label}[{index}]' must be {item_description}")
|