VYPR
High severity7.5OSV Advisory· Published Dec 13, 2025· Updated Apr 15, 2026

CVE-2025-14542

CVE-2025-14542

Description

The vulnerability arises when a client fetches a tools’ JSON specification, known as a Manual, from a remote Manual Endpoint. While a provider may initially serve a benign manual (e.g., one defining an HTTP tool call), earning the clients’ trust, a malicious provider can later change the manual to exploit the client.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
utcpPyPI
< 1.1.01.1.0

Affected products

1

Patches

1
2dc9c02df72c

Update UTCP to 1.1

6 files changed · +580 25
  • core/pyproject.toml+1 1 modified
    @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
     
     [project]
     name = "utcp"
    -version = "1.0.4"
    +version = "1.1.0"
     authors = [
       { name = "UTCP Contributors" },
     ]
    
  • core/README.md+171 15 modified
    @@ -86,6 +86,7 @@ UTCP supports multiple communication protocols through dedicated plugins:
     | [`utcp-cli`](plugins/communication_protocols/cli/) | Command-line tools | ✅ Stable | [CLI Plugin README](plugins/communication_protocols/cli/README.md) |
     | [`utcp-mcp`](plugins/communication_protocols/mcp/) | Model Context Protocol | ✅ Stable | [MCP Plugin README](plugins/communication_protocols/mcp/README.md) |
     | [`utcp-text`](plugins/communication_protocols/text/) | Local file-based tools | ✅ Stable | [Text Plugin README](plugins/communication_protocols/text/README.md) |
    +| [`utcp-websocket`](plugins/communication_protocols/websocket/) | WebSocket real-time bidirectional communication | ✅ Stable | [WebSocket Plugin README](plugins/communication_protocols/websocket/README.md) |
     | [`utcp-socket`](plugins/communication_protocols/socket/) | TCP/UDP protocols | 🚧 In Progress | [Socket Plugin README](plugins/communication_protocols/socket/README.md) |
     | [`utcp-gql`](plugins/communication_protocols/gql/) | GraphQL APIs | 🚧 In Progress | [GraphQL Plugin README](plugins/communication_protocols/gql/README.md) |
     
    @@ -376,12 +377,19 @@ Configuration examples for each protocol. Remember to replace `provider_type` wi
       "url": "https://api.example.com/users/{user_id}", // Required
       "http_method": "POST", // Required, default: "GET"
       "content_type": "application/json", // Optional, default: "application/json"
    -  "auth": { // Optional, example using ApiKeyAuth for a Bearer token. The client must prepend "Bearer " to the token.
    +  "allowed_communication_protocols": ["http"], // Optional, defaults to [call_template_type]. Restricts which protocols tools can use.
    +  "auth": { // Optional, authentication for the HTTP request (example using ApiKeyAuth for Bearer token)
         "auth_type": "api_key",
         "api_key": "Bearer $API_KEY", // Required
         "var_name": "Authorization", // Optional, default: "X-Api-Key"
         "location": "header" // Optional, default: "header"
       },
    +  "auth_tools": { // Optional, authentication for converted tools, if this call template points to an openapi spec that should be automatically converted to a utcp manual (applied only to endpoints requiring auth per OpenAPI spec)
    +    "auth_type": "api_key",
    +    "api_key": "Bearer $TOOL_API_KEY", // Required
    +    "var_name": "Authorization", // Optional, default: "X-Api-Key"
    +    "location": "header" // Optional, default: "header"
    +  },
       "headers": { // Optional
         "X-Custom-Header": "value"
       },
    @@ -437,31 +445,34 @@ Note the name change from `http_stream` to `streamable_http`.
     
     ```json
     {
    -  "name": "my_cli_tool",
    +  "name": "multi_step_cli_tool",
       "call_template_type": "cli", // Required
    -  "commands": [ // Required - array of commands to execute in sequence
    +  "commands": [ // Required - sequential command execution
         {
    -      "command": "cd UTCP_ARG_target_dir_UTCP_END",
    -      "append_to_final_output": false // Optional, default is false if not last command
    +      "command": "git clone UTCP_ARG_repo_url_UTCP_END temp_repo",
    +      "append_to_final_output": false
         },
         {
    -      "command": "my-command --input UTCP_ARG_input_file_UTCP_END"
    -      // append_to_final_output defaults to true for last command
    +      "command": "cd temp_repo && find . -name '*.py' | wc -l"
    +      // Last command output returned by default
         }
       ],
       "env_vars": { // Optional
    -    "MY_VAR": "my_value"
    +    "GIT_AUTHOR_NAME": "UTCP Bot",
    +    "API_KEY": "${MY_API_KEY}"
       },
    -  "working_dir": "/path/to/working/directory", // Optional
    +  "working_dir": "/tmp", // Optional
       "auth": null // Optional (always null for CLI)
     }
     ```
     
    -**Notes:**
    -- Commands execute in a single subprocess (PowerShell on Windows, Bash on Unix)
    -- Use `UTCP_ARG_argname_UTCP_END` placeholders for arguments
    -- Reference previous command output with `$CMD_0_OUTPUT`, `$CMD_1_OUTPUT`, etc.
    -- Only the last command's output is returned by default
    +**CLI Protocol Features:**
    +- **Multi-command execution**: Commands run sequentially in single subprocess
    +- **Cross-platform**: PowerShell on Windows, Bash on Unix/Linux/macOS  
    +- **State preservation**: Directory changes (`cd`) persist between commands
    +- **Argument placeholders**: `UTCP_ARG_argname_UTCP_END` format
    +- **Output referencing**: Access previous outputs with `$CMD_0_OUTPUT`, `$CMD_1_OUTPUT`
    +- **Flexible output control**: Choose which command outputs to include in final result
     
     ### Text Call Template
     
    @@ -470,7 +481,13 @@ Note the name change from `http_stream` to `streamable_http`.
       "name": "my_text_manual",
       "call_template_type": "text", // Required
       "file_path": "./manuals/my_manual.json", // Required
    -  "auth": null // Optional (always null for Text)
    +  "auth": null, // Optional (always null for Text)
    +  "auth_tools": { // Optional, authentication for generated tools from OpenAPI specs
    +    "auth_type": "api_key",
    +    "api_key": "Bearer ${API_TOKEN}",
    +    "var_name": "Authorization",
    +    "location": "header"
    +  }
     }
     ```
     
    @@ -498,6 +515,81 @@ Note the name change from `http_stream` to `streamable_http`.
     }
     ```
     
    +## Security: Protocol Restrictions
    +
    +UTCP provides fine-grained control over which communication protocols each manual can use through the `allowed_communication_protocols` field. This prevents potentially dangerous protocol escalation (e.g., an HTTP-based manual accidentally calling CLI tools).
    +
    +### Default Behavior (Secure by Default)
    +
    +When `allowed_communication_protocols` is not set or is empty, a manual can only register and call tools that use the **same protocol type** as the manual itself:
    +
    +```python
    +from utcp_http.http_call_template import HttpCallTemplate
    +
    +# This manual can ONLY register/call HTTP tools (default restriction)
    +http_manual = HttpCallTemplate(
    +    name="my_api",
    +    call_template_type="http",
    +    url="https://api.example.com/utcp"
    +    # allowed_communication_protocols not set → defaults to ["http"]
    +)
    +```
    +
    +### Allowing Multiple Protocols
    +
    +To allow a manual to work with tools from multiple protocols, explicitly set `allowed_communication_protocols`:
    +
    +```python
    +from utcp_http.http_call_template import HttpCallTemplate
    +
    +# This manual can register/call both HTTP and CLI tools
    +multi_protocol_manual = HttpCallTemplate(
    +    name="flexible_manual",
    +    call_template_type="http",
    +    url="https://api.example.com/utcp",
    +    allowed_communication_protocols=["http", "cli"]  # Explicitly allow both
    +)
    +```
    +
    +### JSON Configuration
    +
    +```json
    +{
    +  "name": "my_api",
    +  "call_template_type": "http",
    +  "url": "https://api.example.com/utcp",
    +  "allowed_communication_protocols": ["http", "cli", "mcp"]
    +}
    +```
    +
    +### Behavior Summary
    +
    +| `allowed_communication_protocols` | Manual Type | Allowed Tool Protocols |
    +|----------------------------------|-------------|------------------------|
    +| Not set / `null` | `"http"` | Only `"http"` |
    +| `[]` (empty) | `"http"` | Only `"http"` |
    +| `["http", "cli"]` | `"http"` | `"http"` and `"cli"` |
    +| `["http", "cli", "mcp"]` | `"cli"` | `"http"`, `"cli"`, and `"mcp"` |
    +
    +### Registration Filtering
    +
    +During `register_manual()`, tools that don't match the allowed protocols are automatically filtered out with a warning:
    +
    +```
    +WARNING - Tool 'dangerous_tool' uses communication protocol 'cli' which is not in 
    +allowed protocols ['http'] for manual 'my_api'. Tool will not be registered.
    +```
    +
    +### Call-Time Validation
    +
    +Even if a tool somehow exists in the repository, calling it will fail if its protocol is not allowed:
    +
    +```python
    +# Raises ValueError: Tool 'my_api.some_cli_tool' uses communication protocol 'cli' 
    +# which is not allowed by manual 'my_api'. Allowed protocols: ['http']
    +await client.call_tool("my_api.some_cli_tool", {"arg": "value"})
    +```
    +
     ## Testing
     
     The testing structure has been updated to reflect the new core/plugin split.
    @@ -535,4 +627,68 @@ The build process now involves building each package (`core` and `plugins`) sepa
     4.  Run the build: `python -m build`.
     5.  The distributable files (`.whl` and `.tar.gz`) will be in the `dist/` directory.
     
    +## OpenAPI Ingestion - Zero Infrastructure Tool Integration
    +
    +🚀 **Transform any existing REST API into UTCP tools without server modifications!**
    +
    +UTCP's OpenAPI ingestion feature automatically converts OpenAPI 2.0/3.0 specifications into UTCP tools, enabling AI agents to interact with existing APIs directly - no wrapper servers, no API changes, no additional infrastructure required.
    +
    +### Quick Start with OpenAPI
    +
    +```python
    +from utcp_http.openapi_converter import OpenApiConverter
    +import aiohttp
    +
    +# Convert any OpenAPI spec to UTCP tools
    +async def convert_api():
    +    async with aiohttp.ClientSession() as session:
    +        async with session.get("https://api.github.com/openapi.json") as response:
    +            openapi_spec = await response.json()
    +    
    +    converter = OpenApiConverter(openapi_spec)
    +    manual = converter.convert()
    +    
    +    print(f"Generated {len(manual.tools)} tools from GitHub API!")
    +    return manual
    +
    +# Or use UTCP Client configuration for automatic detection
    +from utcp.utcp_client import UtcpClient
    +
    +client = await UtcpClient.create(config={
    +    "manual_call_templates": [{
    +        "name": "github",
    +        "call_template_type": "http", 
    +        "url": "https://api.github.com/openapi.json",
    +        "auth_tools": {  # Authentication for generated tools requiring auth
    +            "auth_type": "api_key",
    +            "api_key": "Bearer ${GITHUB_TOKEN}",
    +            "var_name": "Authorization",
    +            "location": "header"
    +        }
    +    }]
    +})
    +```
    +
    +### Key Benefits
    +
    +- ✅ **Zero Infrastructure**: No servers to deploy or maintain
    +- ✅ **Direct API Calls**: Native performance, no proxy overhead  
    +- ✅ **Automatic Conversion**: OpenAPI schemas → UTCP tools
    +- ✅ **Selective Authentication**: Only protected endpoints get auth, public endpoints remain accessible
    +- ✅ **Authentication Preserved**: API keys, OAuth2, Basic auth supported
    +- ✅ **Multi-format Support**: JSON, YAML, OpenAPI 2.0/3.0
    +- ✅ **Batch Processing**: Convert multiple APIs simultaneously
    +
    +### Multiple Ingestion Methods
    +
    +1. **Direct Converter**: `OpenApiConverter` class for full control
    +2. **Remote URLs**: Fetch and convert specs from any URL
    +3. **Client Configuration**: Include specs directly in UTCP config
    +4. **Batch Processing**: Process multiple specs programmatically
    +5. **File-based**: Convert local JSON/YAML specifications
    +
    +📖 **[Complete OpenAPI Ingestion Guide](docs/openapi-ingestion.md)** - Detailed examples and advanced usage
    +
    +---
    +
     ## [Contributors](https://www.utcp.io/about)
    
  • core/src/utcp/data/call_template.py+6 0 modified
    @@ -40,11 +40,17 @@ class CallTemplate(BaseModel):
                 Should be unique across all providers and recommended to be set to a human-readable name.
                 Can only contain letters, numbers and underscores. All special characters must be replaced with underscores.
             call_template_type: The transport protocol type used by this provider.
    +        allowed_communication_protocols: Optional list of communication protocol types that tools
    +            registered under this manual are allowed to use. If None or empty, defaults to only allowing
    +            the same protocol type as the manual's call_template_type. This provides fine-grained security
    +            control - e.g., set to ["http", "cli"] to allow both HTTP and CLI tools, or leave unset to
    +            restrict tools to the manual's own protocol type.
         """
         
         name: str = Field(default_factory=lambda: uuid.uuid4().hex)
         call_template_type: str
         auth: Optional[Auth] = None
    +    allowed_communication_protocols: Optional[List[str]] = None
     
         @field_serializer("auth")
         def serialize_auth(self, auth: Optional[Auth]):
    
  • core/src/utcp/implementations/utcp_client_implementation.py+96 9 modified
    @@ -95,11 +95,26 @@ async def register_manual(self, manual_call_template: CallTemplate) -> RegisterM
             """REQUIRED
             Register a manual in the client.
     
    +        Registers a manual and its tools with the client. During registration, tools are
    +        filtered based on the manual's `allowed_communication_protocols` setting:
    +        
    +        - If `allowed_communication_protocols` is set to a non-empty list, only tools using
    +          protocols in that list are registered.
    +        - If `allowed_communication_protocols` is None or empty, it defaults to only allowing
    +          the manual's own `call_template_type`. This provides secure-by-default behavior.
    +        
    +        Tools that don't match the allowed protocols are excluded from registration and a
    +        warning is logged for each excluded tool.
    +
             Args:
                 manual_call_template: The `CallTemplate` instance representing the manual to register.
     
             Returns:
    -            A `RegisterManualResult` instance representing the result of the registration.
    +            A `RegisterManualResult` instance containing the registered tools (filtered by
    +            allowed protocols) and any errors encountered.
    +
    +        Raises:
    +            ValueError: If manual name is already registered or communication protocol is not found.
             """
             # Replace all non-word characters with underscore
             manual_call_template.name = re.sub(r'[^\w]', '_', manual_call_template.name)
    @@ -112,9 +127,27 @@ async def register_manual(self, manual_call_template: CallTemplate) -> RegisterM
             result = await CommunicationProtocol.communication_protocols[manual_call_template.call_template_type].register_manual(self, manual_call_template)
     
             if result.success:
    +            # Determine allowed protocols: use explicit list or default to manual's own protocol
    +            allowed_protocols = manual_call_template.allowed_communication_protocols
    +            if not allowed_protocols:
    +                allowed_protocols = [manual_call_template.call_template_type]
    +            
    +            # Filter tools based on allowed communication protocols
    +            filtered_tools = []
                 for tool in result.manual.tools:
    -                if not tool.name.startswith(manual_call_template.name + "."):
    -                    tool.name = manual_call_template.name + "." + tool.name
    +                tool_protocol = tool.tool_call_template.call_template_type if tool.tool_call_template else manual_call_template.call_template_type
    +                if tool_protocol in allowed_protocols:
    +                    if not tool.name.startswith(manual_call_template.name + "."):
    +                        tool.name = manual_call_template.name + "." + tool.name
    +                    filtered_tools.append(tool)
    +                else:
    +                    logger.warning(
    +                        f"Tool '{tool.name}' uses communication protocol '{tool_protocol}' "
    +                        f"which is not in allowed protocols {allowed_protocols} for manual '{manual_call_template.name}'. "
    +                        f"Tool will not be registered."
    +                    )
    +            
    +            result.manual.tools = filtered_tools
                 await self.config.tool_repository.save_manual(result.manual_call_template, result.manual)
     
             return result
    @@ -177,19 +210,46 @@ async def call_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
             """REQUIRED
             Call a tool in the client.
     
    +        Executes a registered tool with the provided arguments. Before execution, validates
    +        that the tool's communication protocol is allowed by the parent manual's
    +        `allowed_communication_protocols` setting:
    +        
    +        - If `allowed_communication_protocols` is set to a non-empty list, the tool's protocol
    +          must be in that list.
    +        - If `allowed_communication_protocols` is None or empty, only tools using the manual's
    +          own `call_template_type` are allowed.
    +
             Args:
    -            tool_name: The name of the tool to call.
    +            tool_name: The fully qualified name of the tool (e.g., "manual_name.tool_name").
                 tool_args: A dictionary of arguments to pass to the tool.
     
             Returns:
    -            The result of the tool call.
    +            The result of the tool call, after any post-processing.
    +
    +        Raises:
    +            ValueError: If the tool is not found or if the tool's communication protocol
    +                is not in the manual's allowed protocols.
             """
             manual_name = tool_name.split(".")[0]
             tool = await self.config.tool_repository.get_tool(tool_name)
             if tool is None:
                 raise ValueError(f"Tool not found: {tool_name}")
             tool_call_template = tool.tool_call_template
             tool_call_template = self._substitute_call_template_variables(tool_call_template, manual_name)
    +        
    +        # Check if the tool's communication protocol is allowed by the manual
    +        manual_call_template = await self.config.tool_repository.get_manual_call_template(manual_name)
    +        if manual_call_template:
    +            allowed_protocols = manual_call_template.allowed_communication_protocols
    +            if not allowed_protocols:
    +                allowed_protocols = [manual_call_template.call_template_type]
    +            if tool_call_template.call_template_type not in allowed_protocols:
    +                raise ValueError(
    +                    f"Tool '{tool_name}' uses communication protocol '{tool_call_template.call_template_type}' "
    +                    f"which is not allowed by manual '{manual_name}'. "
    +                    f"Allowed protocols: {allowed_protocols}"
    +                )
    +        
             result = await CommunicationProtocol.communication_protocols[tool_call_template.call_template_type].call_tool(self, tool_name, tool_args, tool_call_template)
             
             for post_processor in self.config.post_processing:
    @@ -198,21 +258,48 @@ async def call_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
     
         async def call_tool_streaming(self, tool_name: str, tool_args: Dict[str, Any]) -> AsyncGenerator[Any, None]:
             """REQUIRED
    -        Call a tool in the client streamingly.
    +        Call a tool in the client with streaming response.
    +
    +        Executes a registered tool with streaming output. Before execution, validates
    +        that the tool's communication protocol is allowed by the parent manual's
    +        `allowed_communication_protocols` setting:
    +        
    +        - If `allowed_communication_protocols` is set to a non-empty list, the tool's protocol
    +          must be in that list.
    +        - If `allowed_communication_protocols` is None or empty, only tools using the manual's
    +          own `call_template_type` are allowed.
     
             Args:
    -            tool_name: The name of the tool to call.
    +            tool_name: The fully qualified name of the tool (e.g., "manual_name.tool_name").
                 tool_args: A dictionary of arguments to pass to the tool.
     
    -        Returns:
    -            An async generator yielding the result of the tool call.
    +        Yields:
    +            Chunks of the tool's streaming response, after any post-processing.
    +
    +        Raises:
    +            ValueError: If the tool is not found or if the tool's communication protocol
    +                is not in the manual's allowed protocols.
             """
             manual_name = tool_name.split(".")[0]
             tool = await self.config.tool_repository.get_tool(tool_name)
             if tool is None:
                 raise ValueError(f"Tool not found: {tool_name}")
             tool_call_template = tool.tool_call_template
             tool_call_template = self._substitute_call_template_variables(tool_call_template, manual_name)
    +        
    +        # Check if the tool's communication protocol is allowed by the manual
    +        manual_call_template = await self.config.tool_repository.get_manual_call_template(manual_name)
    +        if manual_call_template:
    +            allowed_protocols = manual_call_template.allowed_communication_protocols
    +            if not allowed_protocols:
    +                allowed_protocols = [manual_call_template.call_template_type]
    +            if tool_call_template.call_template_type not in allowed_protocols:
    +                raise ValueError(
    +                    f"Tool '{tool_name}' uses communication protocol '{tool_call_template.call_template_type}' "
    +                    f"which is not allowed by manual '{manual_name}'. "
    +                    f"Allowed protocols: {allowed_protocols}"
    +                )
    +        
             async for item in CommunicationProtocol.communication_protocols[tool_call_template.call_template_type].call_tool_streaming(self, tool_name, tool_args, tool_call_template):
                 for post_processor in self.config.post_processing:
                     item = post_processor.post_process(self, tool, tool_call_template, item)
    
  • core/tests/client/test_utcp_client.py+230 0 modified
    @@ -725,6 +725,236 @@ async def test_load_call_templates_wrong_format(self):
                 os.unlink(temp_file)
     
     
    +class TestAllowedCommunicationProtocols:
    +    """Test allowed_communication_protocols restriction functionality."""
    +
    +    @pytest.mark.asyncio
    +    async def test_call_tool_allowed_protocol(self, utcp_client, sample_tools, isolated_communication_protocols):
    +        """Test calling a tool when its protocol is in the allowed list."""
    +        client = utcp_client
    +        call_template = HttpCallTemplate(
    +            name="test_manual",
    +            url="https://api.example.com/tool",
    +            http_method="POST",
    +            call_template_type="http",
    +            allowed_communication_protocols=["http", "cli"]  # Allow both HTTP and CLI
    +        )
    +        
    +        manual = UtcpManual(utcp_version="1.0", manual_version="1.0", tools=sample_tools[:1])
    +        mock_protocol = MockCommunicationProtocol(manual, "test_result")
    +        CommunicationProtocol.communication_protocols["http"] = mock_protocol
    +        
    +        await client.register_manual(call_template)
    +        
    +        # Call should succeed since "http" is in allowed_communication_protocols
    +        result = await client.call_tool("test_manual.http_tool", {"param1": "value1"})
    +        assert result == "test_result"
    +
    +    @pytest.mark.asyncio
    +    async def test_register_filters_disallowed_protocol_tools(self, utcp_client, sample_tools, isolated_communication_protocols):
    +        """Test that tools with disallowed protocols are filtered during registration."""
    +        client = utcp_client
    +        
    +        # Register HTTP manual that only allows "http" protocol
    +        http_call_template = HttpCallTemplate(
    +            name="http_manual",
    +            url="https://api.example.com/tool",
    +            http_method="POST",
    +            call_template_type="http",
    +            allowed_communication_protocols=["http"]  # Only allow HTTP
    +        )
    +        
    +        # Create a tool that uses CLI protocol (which is not allowed)
    +        cli_tool = Tool(
    +            name="cli_tool",
    +            description="CLI test tool",
    +            inputs=JsonSchema(
    +                type="object",
    +                properties={"command": {"type": "string", "description": "Command to execute"}},
    +                required=["command"]
    +            ),
    +            outputs=JsonSchema(
    +                type="object",
    +                properties={"output": {"type": "string", "description": "Command output"}}
    +            ),
    +            tags=["cli", "test"],
    +            tool_call_template=CliCallTemplate(
    +                name="cli_provider",
    +                commands=[{"command": "echo UTCP_ARG_command_UTCP_END"}],
    +                call_template_type="cli"
    +            )
    +        )
    +        
    +        manual = UtcpManual(utcp_version="1.0", manual_version="1.0", tools=[cli_tool])
    +        mock_http_protocol = MockCommunicationProtocol(manual)
    +        mock_cli_protocol = MockCommunicationProtocol()
    +        CommunicationProtocol.communication_protocols["http"] = mock_http_protocol
    +        CommunicationProtocol.communication_protocols["cli"] = mock_cli_protocol
    +        
    +        result = await client.register_manual(http_call_template)
    +        
    +        # CLI tool should be filtered out during registration
    +        assert len(result.manual.tools) == 0
    +        
    +        # Tool should not exist in repository
    +        tool = await client.config.tool_repository.get_tool("http_manual.cli_tool")
    +        assert tool is None
    +
    +    @pytest.mark.asyncio
    +    async def test_call_tool_default_protocol_restriction(self, utcp_client, sample_tools, isolated_communication_protocols):
    +        """Test that when no allowed_communication_protocols is set, only the manual's protocol is allowed."""
    +        client = utcp_client
    +        
    +        # Register HTTP manual without explicit protocol restrictions
    +        # Default behavior: only HTTP tools should be allowed
    +        http_call_template = HttpCallTemplate(
    +            name="http_manual",
    +            url="https://api.example.com/tool",
    +            http_method="POST",
    +            call_template_type="http"
    +            # No allowed_communication_protocols set - defaults to ["http"]
    +        )
    +        
    +        # Create tools: one HTTP (should be registered), one CLI (should be filtered out)
    +        http_tool = Tool(
    +            name="http_tool",
    +            description="HTTP test tool",
    +            inputs=JsonSchema(type="object", properties={}),
    +            outputs=JsonSchema(type="object", properties={}),
    +            tool_call_template=HttpCallTemplate(
    +                name="http_provider",
    +                url="https://api.example.com/call",
    +                http_method="GET",
    +                call_template_type="http"
    +            )
    +        )
    +        cli_tool = Tool(
    +            name="cli_tool",
    +            description="CLI test tool",
    +            inputs=JsonSchema(type="object", properties={}),
    +            outputs=JsonSchema(type="object", properties={}),
    +            tool_call_template=CliCallTemplate(
    +                name="cli_provider",
    +                commands=[{"command": "echo test"}],
    +                call_template_type="cli"
    +            )
    +        )
    +        
    +        manual = UtcpManual(utcp_version="1.0", manual_version="1.0", tools=[http_tool, cli_tool])
    +        mock_http_protocol = MockCommunicationProtocol(manual, call_result="http_result")
    +        mock_cli_protocol = MockCommunicationProtocol()
    +        CommunicationProtocol.communication_protocols["http"] = mock_http_protocol
    +        CommunicationProtocol.communication_protocols["cli"] = mock_cli_protocol
    +        
    +        result = await client.register_manual(http_call_template)
    +        
    +        # Only HTTP tool should be registered, CLI tool should be filtered out
    +        assert len(result.manual.tools) == 1
    +        assert result.manual.tools[0].name == "http_manual.http_tool"
    +        
    +        # HTTP tool call should succeed
    +        call_result = await client.call_tool("http_manual.http_tool", {})
    +        assert call_result == "http_result"
    +        
    +        # CLI tool should not exist in repository
    +        cli_tool_in_repo = await client.config.tool_repository.get_tool("http_manual.cli_tool")
    +        assert cli_tool_in_repo is None
    +
    +    @pytest.mark.asyncio
    +    async def test_register_with_multiple_allowed_protocols(self, utcp_client, sample_tools, isolated_communication_protocols):
    +        """Test registration with multiple allowed protocols allows all specified types."""
    +        client = utcp_client
    +        
    +        http_call_template = HttpCallTemplate(
    +            name="multi_protocol_manual",
    +            url="https://api.example.com/tool",
    +            http_method="POST",
    +            call_template_type="http",
    +            allowed_communication_protocols=["http", "cli"]  # Allow both
    +        )
    +        
    +        http_tool = Tool(
    +            name="http_tool",
    +            description="HTTP test tool",
    +            inputs=JsonSchema(type="object", properties={}),
    +            outputs=JsonSchema(type="object", properties={}),
    +            tool_call_template=HttpCallTemplate(
    +                name="http_provider",
    +                url="https://api.example.com/call",
    +                http_method="GET",
    +                call_template_type="http"
    +            )
    +        )
    +        cli_tool = Tool(
    +            name="cli_tool",
    +            description="CLI test tool",
    +            inputs=JsonSchema(type="object", properties={}),
    +            outputs=JsonSchema(type="object", properties={}),
    +            tool_call_template=CliCallTemplate(
    +                name="cli_provider",
    +                commands=[{"command": "echo test"}],
    +                call_template_type="cli"
    +            )
    +        )
    +        
    +        manual = UtcpManual(utcp_version="1.0", manual_version="1.0", tools=[http_tool, cli_tool])
    +        mock_http_protocol = MockCommunicationProtocol(manual, call_result="http_result")
    +        mock_cli_protocol = MockCommunicationProtocol(call_result="cli_result")
    +        CommunicationProtocol.communication_protocols["http"] = mock_http_protocol
    +        CommunicationProtocol.communication_protocols["cli"] = mock_cli_protocol
    +        
    +        result = await client.register_manual(http_call_template)
    +        
    +        # Both tools should be registered
    +        assert len(result.manual.tools) == 2
    +        tool_names = [t.name for t in result.manual.tools]
    +        assert "multi_protocol_manual.http_tool" in tool_names
    +        assert "multi_protocol_manual.cli_tool" in tool_names
    +        
    +        # Both tools should be callable
    +        http_result = await client.call_tool("multi_protocol_manual.http_tool", {})
    +        assert http_result == "http_result"
    +        
    +        cli_result = await client.call_tool("multi_protocol_manual.cli_tool", {})
    +        assert cli_result == "cli_result"
    +
    +    @pytest.mark.asyncio
    +    async def test_call_tool_empty_allowed_protocols_defaults_to_manual_type(self, utcp_client, sample_tools, isolated_communication_protocols):
    +        """Test that empty allowed_communication_protocols defaults to manual's protocol type."""
    +        client = utcp_client
    +        
    +        http_call_template = HttpCallTemplate(
    +            name="http_manual",
    +            url="https://api.example.com/tool",
    +            http_method="POST",
    +            call_template_type="http",
    +            allowed_communication_protocols=[]  # Empty list defaults to ["http"]
    +        )
    +        
    +        cli_tool = Tool(
    +            name="cli_tool",
    +            description="CLI test tool",
    +            inputs=JsonSchema(type="object", properties={}),
    +            outputs=JsonSchema(type="object", properties={}),
    +            tool_call_template=CliCallTemplate(
    +                name="cli_provider",
    +                commands=[{"command": "echo test"}],
    +                call_template_type="cli"
    +            )
    +        )
    +        
    +        manual = UtcpManual(utcp_version="1.0", manual_version="1.0", tools=[cli_tool])
    +        mock_http_protocol = MockCommunicationProtocol(manual)
    +        mock_cli_protocol = MockCommunicationProtocol(call_result="cli_result")
    +        CommunicationProtocol.communication_protocols["http"] = mock_http_protocol
    +        CommunicationProtocol.communication_protocols["cli"] = mock_cli_protocol
    +        
    +        result = await client.register_manual(http_call_template)
    +        
    +        # CLI tool should be filtered out during registration
    +        assert len(result.manual.tools) == 0
    +
    +
     class TestToolSerialization:
         """Test Tool and JsonSchema serialization."""
     
    
  • README.md+76 0 modified
    @@ -377,6 +377,7 @@ Configuration examples for each protocol. Remember to replace `provider_type` wi
       "url": "https://api.example.com/users/{user_id}", // Required
       "http_method": "POST", // Required, default: "GET"
       "content_type": "application/json", // Optional, default: "application/json"
    +  "allowed_communication_protocols": ["http"], // Optional, defaults to [call_template_type]. Restricts which protocols tools can use.
       "auth": { // Optional, authentication for the HTTP request (example using ApiKeyAuth for Bearer token)
         "auth_type": "api_key",
         "api_key": "Bearer $API_KEY", // Required
    @@ -514,6 +515,81 @@ Note the name change from `http_stream` to `streamable_http`.
     }
     ```
     
    +## Security: Protocol Restrictions
    +
    +UTCP provides fine-grained control over which communication protocols each manual can use through the `allowed_communication_protocols` field. This prevents potentially dangerous protocol escalation (e.g., an HTTP-based manual accidentally calling CLI tools).
    +
    +### Default Behavior (Secure by Default)
    +
    +When `allowed_communication_protocols` is not set or is empty, a manual can only register and call tools that use the **same protocol type** as the manual itself:
    +
    +```python
    +from utcp_http.http_call_template import HttpCallTemplate
    +
    +# This manual can ONLY register/call HTTP tools (default restriction)
    +http_manual = HttpCallTemplate(
    +    name="my_api",
    +    call_template_type="http",
    +    url="https://api.example.com/utcp"
    +    # allowed_communication_protocols not set → defaults to ["http"]
    +)
    +```
    +
    +### Allowing Multiple Protocols
    +
    +To allow a manual to work with tools from multiple protocols, explicitly set `allowed_communication_protocols`:
    +
    +```python
    +from utcp_http.http_call_template import HttpCallTemplate
    +
    +# This manual can register/call both HTTP and CLI tools
    +multi_protocol_manual = HttpCallTemplate(
    +    name="flexible_manual",
    +    call_template_type="http",
    +    url="https://api.example.com/utcp",
    +    allowed_communication_protocols=["http", "cli"]  # Explicitly allow both
    +)
    +```
    +
    +### JSON Configuration
    +
    +```json
    +{
    +  "name": "my_api",
    +  "call_template_type": "http",
    +  "url": "https://api.example.com/utcp",
    +  "allowed_communication_protocols": ["http", "cli", "mcp"]
    +}
    +```
    +
    +### Behavior Summary
    +
    +| `allowed_communication_protocols` | Manual Type | Allowed Tool Protocols |
    +|----------------------------------|-------------|------------------------|
    +| Not set / `null` | `"http"` | Only `"http"` |
    +| `[]` (empty) | `"http"` | Only `"http"` |
    +| `["http", "cli"]` | `"http"` | `"http"` and `"cli"` |
    +| `["http", "cli", "mcp"]` | `"cli"` | `"http"`, `"cli"`, and `"mcp"` |
    +
    +### Registration Filtering
    +
    +During `register_manual()`, tools that don't match the allowed protocols are automatically filtered out with a warning:
    +
    +```
    +WARNING - Tool 'dangerous_tool' uses communication protocol 'cli' which is not in 
    +allowed protocols ['http'] for manual 'my_api'. Tool will not be registered.
    +```
    +
    +### Call-Time Validation
    +
    +Even if a tool somehow exists in the repository, calling it will fail if its protocol is not allowed:
    +
    +```python
    +# Raises ValueError: Tool 'my_api.some_cli_tool' uses communication protocol 'cli' 
    +# which is not allowed by manual 'my_api'. Allowed protocols: ['http']
    +await client.call_tool("my_api.some_cli_tool", {"arg": "value"})
    +```
    +
     ## Testing
     
     The testing structure has been updated to reflect the new core/plugin split.
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.