mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-17 07:17:18 +00:00
feat(agent-runner): enforce 4.x host-owned execution
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""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}"
|
||||
)
|
||||
Reference in New Issue
Block a user