Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/pyagentspec/source/api/adapters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ into the equivalent solution, as per each framework's definition, and return an

This page presents all APIs and classes related to Agent Spec Adapters.

Shared Loader
-------------

.. _adapters_shared_loader:
.. autoclass:: pyagentspec.adapters._agentspecloader.AdapterAgnosticAgentSpecLoader

LangGraph
---------

Expand Down
14 changes: 14 additions & 0 deletions docs/pyagentspec/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ Improvements
directly in the source template instead of recursively splitting the template text.
This keeps unresolved placeholders unchanged and makes the rendering logic easier to follow.

* **Component loading defaults**

Agent Spec loaders now expose ``allowed_components`` and ``blocked_components`` options,
allowing users to control which Agent Spec component types can be loaded from configurations.
Component type names that resolve to known Component classes use hierarchy matching,
like class entries; unresolved type names match only the exact serialized component type.

New features
^^^^^^^^^^^^
Expand Down Expand Up @@ -202,6 +208,14 @@ Property titles in Agent Spec must not be empty. This is now enforced by validat

Migration: If your YAML/JSON configurations have properties without titles, you’ll need to set a non-empty, descriptive title for those properties to pass validation. If you generate Agent Spec configurations via the SDK, your code may still work, but we recommend explicitly setting property titles to ensure forward compatibility.

* **MCP stdio transport is blocked by default in Agent Spec loaders**

``StdioTransport`` and its subclasses will no longer load by default through
Agent Spec loaders. If a trusted configuration
intentionally uses stdio transports, pass ``blocked_components=[]`` to the
loader, or provide a custom ``blocked_components`` value that does not include
the stdio transport component class.


Agent Spec 26.1.0
-----------------
Expand Down
21 changes: 21 additions & 0 deletions docs/pyagentspec/source/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,27 @@ utilities to avoid unsafe behavior.

When deploying serialized representation to an execution engine, use the engine's secure loading mechanism for the import.

Considerations regarding runtime-sensitive components
-----------------------------------------------------

Some Agent Spec components can influence actions performed by adapters or runtimes
when a configuration is loaded or run. For example, MCP stdio transports can start
a local MCP server subprocess on the machine that loads and runs the resulting
agent.

Only enable runtime-sensitive components from trusted configurations. PyAgentSpec
loaders block ``StdioTransport`` and its subclasses by default. If a trusted
configuration intentionally uses stdio transports, pass
``blocked_components=[]`` to the loader, or provide a custom
``blocked_components`` value that does not include the stdio transport
component class. For stricter environments, use ``allowed_components`` to
explicitly list the component types that may be loaded.

Component policy entries can be provided as Component classes or component type
names. Type names that resolve to known Component classes use the same hierarchy
matching as class entries; unresolved type names match only the exact serialized
component type.

.. _securitycatchexceptionnode:

Considerations regarding exception handling in Flows
Expand Down
1 change: 1 addition & 0 deletions pyagentspec/constraints/constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ crewai==1.6.1
langgraph==1.0.5
langchain==1.2.0
langchain-openai==1.1.7
openai==2.35.1
langchain-ollama==1.0.1
langchain-oci==0.2.3
langchain-mcp-adapters==0.2.1
Expand Down
45 changes: 43 additions & 2 deletions pyagentspec/src/pyagentspec/adapters/_agentspecloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,27 @@

import logging
from abc import ABC, abstractmethod
from typing import Any, Callable, Dict, List, Optional, Protocol, TypeAlias, Union, cast, overload
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Protocol,
TypeAlias,
Union,
cast,
overload,
)

from pyagentspec.component import Component as AgentSpecComponent
from pyagentspec.mcp import StdioTransport
from pyagentspec.serialization import AgentSpecDeserializer, ComponentDeserializationPlugin
from pyagentspec.serialization.componentpolicy import ComponentLoadPolicy, ComponentPolicyInput

_RuntimeComponentT: TypeAlias = Any
_RuntimeRegistryT: TypeAlias = Dict[str, Any]
_DEFAULT_BLOCKED_COMPONENTS: tuple[type[AgentSpecComponent], ...] = (StdioTransport,)


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -66,15 +80,37 @@ class AdapterAgnosticAgentSpecLoader(ABC):
Optional callable to convert a runtime components registry into an Agent
Spec registry (mapping ids to Agent Spec components/values) so that
references can be resolved during deserialization.
allowed_components:
Optional iterable of Agent Spec component type names or Component classes allowed
to be loaded. If omitted, all component types are allowed unless blocked.
blocked_components:
Optional iterable of Agent Spec component type names or Component classes blocked
from loading. If omitted, ``StdioTransport`` and its subclasses are blocked by default.
Resolvable type names and Component classes also match subclasses; unresolved
type names match only the exact serialized component type. When allow and
block entries both match, the closest match in the component class hierarchy
wins; block entries win same-distance ties.
"""

def __init__(
self,
tool_registry: Optional[Dict[str, Any]] = None,
plugins: Optional[List[ComponentDeserializationPlugin]] = None,
*,
allowed_components: Optional[ComponentPolicyInput] = None,
blocked_components: Optional[ComponentPolicyInput] = None,
) -> None:
self.plugins = plugins
self.tool_registry = tool_registry or {}
blocked_components = (
_DEFAULT_BLOCKED_COMPONENTS if blocked_components is None else blocked_components
)
self.component_load_policy = ComponentLoadPolicy(
allowed_components=allowed_components,
blocked_components=blocked_components,
)
self.allowed_components = self.component_load_policy.allowed_components
self.blocked_components = self.component_load_policy.blocked_components

@property
@abstractmethod
Expand Down Expand Up @@ -296,6 +332,7 @@ def load_component(self, agentspec_component: AgentSpecComponent) -> _RuntimeCom
Subclasses may override this method to pass adapter-specific parameters
into their converter (e.g., tool registries).
"""
self.component_load_policy.validate_component_tree(agentspec_component)
return self.agentspec_to_runtime_converter.convert(
agentspec_component, tool_registry=self.tool_registry
)
Expand Down Expand Up @@ -326,7 +363,11 @@ def _load(
import_only_referenced_components: bool,
) -> Union[_RuntimeComponentT, Dict[str, _RuntimeComponentT]]:

deserializer = AgentSpecDeserializer(plugins=self.plugins)
deserializer = AgentSpecDeserializer(
plugins=self.plugins,
allowed_components=self.allowed_components,
blocked_components=self.blocked_components,
)
deserializer_func: Callable[..., Any]
if loader == "yaml":
deserializer_func = deserializer.from_yaml
Expand Down
21 changes: 19 additions & 2 deletions pyagentspec/src/pyagentspec/adapters/crewai/agentspecloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,36 @@
)
from pyagentspec.adapters.crewai._agentspecconverter import CrewAIToAgentSpecConverter
from pyagentspec.adapters.crewai._crewaiconverter import AgentSpecToCrewAIConverter
from pyagentspec.serialization.componentpolicy import ComponentPolicyInput


class AgentSpecLoader(AdapterAgnosticAgentSpecLoader):
"""Helper class to convert Agent Spec configurations to CrewAI objects."""
"""Helper class to convert Agent Spec configurations to CrewAI objects.

``allowed_components`` and ``blocked_components`` can be used to constrain
which Agent Spec component types load. Resolvable type names and Component
classes match subclasses; unresolved type names match only the exact serialized
component type. When allow and block entries both match, the closest match
in the component class hierarchy wins; block entries win same-distance ties.
If ``blocked_components`` is omitted, ``StdioTransport`` and its subclasses
are blocked by default.
"""

def __init__(
self,
tool_registry: Optional[Dict[str, Any]] = None,
plugins: Optional[List[Any]] = None,
*,
allowed_components: Optional[ComponentPolicyInput] = None,
blocked_components: Optional[ComponentPolicyInput] = None,
Comment thread
ljupche98 marked this conversation as resolved.
enable_agentspec_tracing: bool = True,
) -> None:
super().__init__(tool_registry=tool_registry, plugins=plugins)
super().__init__(
tool_registry=tool_registry,
plugins=plugins,
allowed_components=allowed_components,
blocked_components=blocked_components,
)
self._enable_agentspec_tracing = enable_agentspec_tracing

@property
Expand Down
24 changes: 22 additions & 2 deletions pyagentspec/src/pyagentspec/adapters/langgraph/agentspecloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)
from pyagentspec.component import Component as AgentSpecComponent
from pyagentspec.serialization import ComponentDeserializationPlugin
from pyagentspec.serialization.componentpolicy import ComponentPolicyInput


class AgentSpecLoader(AdapterAgnosticAgentSpecLoader):
Expand All @@ -38,6 +39,16 @@ class AgentSpecLoader(AdapterAgnosticAgentSpecLoader):
enables features that require a checkpointer (e.g., client tools).
config:
Optional ``RunnableConfig`` to pass to created runnables/graphs.
allowed_components:
Optional iterable of Agent Spec component type names or Component classes allowed
to be loaded. If omitted, all component types are allowed unless blocked.
blocked_components:
Optional iterable of Agent Spec component type names or Component classes blocked
from loading. If omitted, stdio MCP transports are blocked by default.
Resolvable type names and Component classes also match subclasses; unresolved
type names match only the exact serialized component type. When allow and
block entries both match, the closest match in the component class hierarchy
wins; block entries win same-distance ties.
"""

def __init__(
Expand All @@ -46,8 +57,16 @@ def __init__(
plugins: Optional[List[ComponentDeserializationPlugin]] = None,
checkpointer: Optional[Checkpointer] = None,
config: Optional[RunnableConfig] = None,
*,
allowed_components: Optional[ComponentPolicyInput] = None,
blocked_components: Optional[ComponentPolicyInput] = None,
) -> None:
super().__init__(plugins=plugins, tool_registry=tool_registry)
super().__init__(
plugins=plugins,
tool_registry=tool_registry,
allowed_components=allowed_components,
blocked_components=blocked_components,
)
self.checkpointer = checkpointer
self.config = config

Expand Down Expand Up @@ -259,7 +278,8 @@ def load_dict(
)

def load_component(self, agentspec_component: AgentSpecComponent) -> LangGraphRuntimeComponent:
# Need to override to make it use config and checkpointer
# Need to override to make it use config and checkpointer, while preserving base policy validation
self.component_load_policy.validate_component_tree(agentspec_component)
return cast(
LangGraphRuntimeComponent,
self.agentspec_to_runtime_converter.convert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pyagentspec.component import Component as AgentSpecComponent
from pyagentspec.flows.flow import Flow as AgentSpecFlow
from pyagentspec.serialization import ComponentDeserializationPlugin
from pyagentspec.serialization.componentpolicy import ComponentPolicyInput

_OAComponent = Union[OAAgent, str]

Expand Down Expand Up @@ -48,6 +49,9 @@ def _load_component(
"""
is_flow = isinstance(agentspec_component, cast(type, AgentSpecFlow))
if is_flow:
# Flow codegen bypasses the base loader's conversion path, so enforce the
# component policy here before generating executable Python source.
agent_spec_loader.component_load_policy.validate_component_tree(agentspec_component)
pack = resolve_rulepack(rulepack_version)
ir = pack.agentspec_to_ir(agentspec_component, strict=True)
mod = pack.codegen(ir, module_name=module_name)
Expand All @@ -61,12 +65,22 @@ def _load_component(


class AgentSpecLoader(AdapterAgnosticAgentSpecLoader):
"""Helper class to convert Agent Spec configurations to OpenAI Agents SDK objects."""
"""Helper class to convert Agent Spec configurations to OpenAI Agents SDK objects.

``allowed_components`` and ``blocked_components`` can be used to constrain
which Agent Spec component types load. Resolvable type names and Component
classes match subclasses; unresolved type names match only the exact serialized
component type. When allow and block entries both match, the closest match
in the component class hierarchy wins; block entries win same-distance ties.
"""

def __init__(
self,
tool_registry: Optional[Dict[str, _TargetTool]] = None,
plugins: Optional[List[ComponentDeserializationPlugin]] = None,
*,
allowed_components: Optional[ComponentPolicyInput] = None,
blocked_components: Optional[ComponentPolicyInput] = None,
):
"""
Parameters
Expand All @@ -77,8 +91,23 @@ def __init__(
the values are the tool objects (prebuilt OpenAI FunctionTool or callable).
plugins:
Optional list of deserialization plugins for PyAgentSpec.
allowed_components:
Optional iterable of Agent Spec component type names or Component classes allowed
to be loaded. If omitted, all component types are allowed unless blocked.
blocked_components:
Optional iterable of Agent Spec component type names or Component classes blocked
from loading. If omitted, stdio MCP transports are blocked by default.
Resolvable type names and Component classes also match subclasses; unresolved
type names match only the exact serialized component type. When allow and
block entries both match, the closest match in the component class hierarchy
wins; block entries win same-distance ties.
"""
super().__init__(plugins=plugins, tool_registry=tool_registry)
super().__init__(
plugins=plugins,
tool_registry=tool_registry,
allowed_components=allowed_components,
blocked_components=blocked_components,
)

@property
def agentspec_to_runtime_converter(self) -> AgentSpecToOpenAIConverter:
Expand Down
Loading
Loading