feat(agent-runner): enforce 4.x host-owned execution

This commit is contained in:
huanghuoguoguo
2026-07-12 20:36:32 +08:00
parent 9aa71d54e3
commit d88d05e27c
170 changed files with 6952 additions and 5381 deletions
+10 -16
View File
@@ -7,6 +7,7 @@ import typing
import sqlalchemy
from ....core import app
from ....agent.runner.config_resolver import RunnerConfigResolver
from ....entity.persistence import agent as persistence_agent
@@ -82,8 +83,8 @@ class AgentService:
if kind != AGENT_KIND_AGENT:
raise ValueError(f'Unsupported agent kind: {kind}')
config = agent_data.get('config') or await self._get_default_agent_config()
runner_id = self._resolve_runner_id(config)
config = agent_data['config'] if 'config' in agent_data else await self._get_default_agent_config()
config, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(config)
new_uuid = str(uuid.uuid4())
values = {
'uuid': new_uuid,
@@ -91,7 +92,7 @@ class AgentService:
'description': agent_data.get('description') or '',
'emoji': agent_data.get('emoji') or '🤖',
'kind': AGENT_KIND_AGENT,
'component_ref': agent_data.get('component_ref') or runner_id,
'component_ref': runner_id,
'config': config,
'enabled': agent_data.get('enabled', True),
'supported_event_patterns': agent_data.get('supported_event_patterns') or AGENT_DEFAULT_EVENT_PATTERNS,
@@ -109,12 +110,14 @@ class AgentService:
return
update_data = agent_data.copy()
for protected_field in ('uuid', 'kind', 'created_at', 'updated_at', 'capability'):
for protected_field in ('uuid', 'kind', 'component_ref', 'created_at', 'updated_at', 'capability'):
update_data.pop(protected_field, None)
if 'config' in update_data:
update_data['component_ref'] = update_data.get('component_ref') or self._resolve_runner_id(
update_data['config']
)
config, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(update_data['config'])
update_data['config'] = config
else:
_, runner_id, _ = RunnerConfigResolver.resolve_agent_runner_config(existing_agent.config)
update_data['component_ref'] = runner_id
if 'supported_event_patterns' in update_data and not update_data['supported_event_patterns']:
update_data['supported_event_patterns'] = AGENT_DEFAULT_EVENT_PATTERNS
@@ -169,15 +172,6 @@ class AgentService:
},
}
@staticmethod
def _resolve_runner_id(config: dict[str, typing.Any]) -> str | None:
runner = config.get('runner') if isinstance(config, dict) else None
if isinstance(runner, dict):
runner_id = runner.get('id')
if runner_id:
return runner_id
return None
def _agent_to_product_item(
self,
agent: persistence_agent.Agent,
+66 -13
View File
@@ -6,7 +6,12 @@ import sqlalchemy
import typing
from ....core import app
from ....agent.runner.config_resolver import RunnerConfigResolver
from ....entity.persistence import pipeline as persistence_pipeline
from ....pipeline.extension_preferences import (
normalize_extension_preferences,
validate_extension_preferences,
)
default_stage_order = [
@@ -163,6 +168,11 @@ class PipelineService:
return self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
async def create_pipeline(self, pipeline_data: dict, default: bool = False) -> str:
if 'extensions_preferences' in pipeline_data:
self._validate_extension_preferences(pipeline_data['extensions_preferences'])
if 'config' in pipeline_data:
RunnerConfigResolver.validate_pipeline_config(pipeline_data['config'])
# Check limitation
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
max_pipelines = limitation.get('max_pipelines', -1)
@@ -177,6 +187,7 @@ class PipelineService:
pipeline_data['is_default'] = default
pipeline_data['config'] = await self.get_default_pipeline_config()
RunnerConfigResolver.validate_pipeline_config(pipeline_data['config'])
# Ensure extensions_preferences is set with enable_all_plugins and enable_all_mcp_servers=True by default
if 'extensions_preferences' not in pipeline_data:
@@ -203,6 +214,10 @@ class PipelineService:
pipeline_data = pipeline_data.copy()
for protected_field in ('uuid', 'for_version', 'stages', 'is_default'):
pipeline_data.pop(protected_field, None)
if 'config' in pipeline_data:
RunnerConfigResolver.validate_pipeline_config(pipeline_data['config'])
if 'extensions_preferences' in pipeline_data:
self._validate_extension_preferences(pipeline_data['extensions_preferences'])
await self.ap.persistence_mgr.execute_async(
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
@@ -259,18 +274,7 @@ class PipelineService:
'stages': original_pipeline.stages.copy() if original_pipeline.stages else default_stage_order.copy(),
'config': original_pipeline.config.copy() if original_pipeline.config else {},
'is_default': False,
'extensions_preferences': (
original_pipeline.extensions_preferences.copy()
if original_pipeline.extensions_preferences
else {
'enable_all_plugins': True,
'enable_all_mcp_servers': True,
'plugins': [],
'mcp_servers': [],
'mcp_resources': [],
'mcp_resource_agent_read_enabled': True,
}
),
'extensions_preferences': normalize_extension_preferences(original_pipeline.extensions_preferences),
}
# Insert the new pipeline
@@ -297,6 +301,36 @@ class PipelineService:
mcp_resource_agent_read_enabled: bool | None = None,
) -> None:
"""Update the bound plugins and MCP servers for a pipeline"""
extension_updates: dict[str, typing.Any] = {
'enable_all_plugins': enable_all_plugins,
'enable_all_mcp_servers': enable_all_mcp_servers,
'enable_all_skills': enable_all_skills,
'plugins': bound_plugins,
}
if bound_mcp_servers is not None:
extension_updates['mcp_servers'] = bound_mcp_servers
if bound_skills is not None:
extension_updates['skills'] = bound_skills
if bound_mcp_resources is not None:
extension_updates['mcp_resources'] = bound_mcp_resources
RunnerConfigResolver.validate_mcp_resource_attachments(
bound_mcp_resources,
context='Pipeline extension',
field_name='bound_mcp_resources',
)
if mcp_resource_agent_read_enabled is not None:
extension_updates['mcp_resource_agent_read_enabled'] = mcp_resource_agent_read_enabled
self._validate_extension_preferences(
extension_updates,
context='Pipeline extension',
field_aliases={
'plugins': 'bound_plugins',
'mcp_servers': 'bound_mcp_servers',
'skills': 'bound_skills',
'mcp_resources': 'bound_mcp_resources',
},
)
# Get current pipeline
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
@@ -309,7 +343,7 @@ class PipelineService:
raise ValueError(f'Pipeline {pipeline_uuid} not found')
# Update extensions_preferences
extensions_preferences = pipeline.extensions_preferences or {}
extensions_preferences = normalize_extension_preferences(pipeline.extensions_preferences)
extensions_preferences['enable_all_plugins'] = enable_all_plugins
extensions_preferences['enable_all_mcp_servers'] = enable_all_mcp_servers
extensions_preferences['enable_all_skills'] = enable_all_skills
@@ -333,3 +367,22 @@ class PipelineService:
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
pipeline = await self.get_pipeline(pipeline_uuid)
await self.ap.pipeline_mgr.load_pipeline(pipeline)
@staticmethod
def _validate_extension_preferences(
value: typing.Any,
*,
context: str = 'Pipeline extensions_preferences',
field_aliases: typing.Mapping[str, str] | None = None,
) -> dict[str, typing.Any]:
validated = validate_extension_preferences(
value,
context=context,
field_aliases=field_aliases,
)
RunnerConfigResolver.validate_mcp_resource_attachments(
validated.get('mcp_resources'),
context=context,
field_name='mcp_resources',
)
return validated