fix(extension): 完成MCP命令目标并收紧Schema边界

This commit is contained in:
2026-09-02 14:52:03 +08:00
parent c1bac00d12
commit cff38158f6
14 changed files with 403 additions and 28 deletions
+20 -5
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
import hashlib
import inspect
import json
import math
@@ -17,7 +18,7 @@ from typing import Any, Awaitable, Callable, Literal
from jsonschema import Draft202012Validator
from jsonschema.exceptions import SchemaError, ValidationError as JsonSchemaValidationError
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator
from app.config import get_settings
from app.contracts import (
@@ -34,6 +35,10 @@ from app.contracts import (
)
from app.extensions.errors import ExtensionError
from app.providers.credentials import CredentialStoreError, EncryptedCredentialStore
from app.schema_security import (
SchemaReferenceError,
reject_external_schema_references,
)
_CONTRIBUTION_ID = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
_SETTING_KEY = re.compile(r"^[a-z][a-z0-9._-]{0,127}$")
@@ -74,9 +79,16 @@ class PluginCommandSpec(BaseModel):
)
permission: str | None = None
secrets: list[str] = Field(default_factory=list)
handler: Literal["echo", "uppercase_selection"]
handler: Literal["echo", "uppercase_selection"] | None = None
mcp_tool: str | None = None
timeout_seconds: int = Field(default=30, ge=1, le=120)
@model_validator(mode="after")
def validate_execution_target(self) -> "PluginCommandSpec":
if (self.handler is None) == (self.mcp_tool is None):
raise ValueError("Command must declare exactly one handler or mcp_tool target.")
return self
CommandExecutor = Callable[
[dict[str, Any], dict[str, Any]],
@@ -689,11 +701,13 @@ def validate_command_spec(plugin_id: str, spec: PluginCommandSpec) -> None:
if spec.parameters.get("type", "object") != "object":
raise ExtensionError("PLUGIN_COMMAND_INVALID", "Command parameters must be an object schema.")
try:
reject_external_schema_references(spec.parameters)
Draft202012Validator.check_schema(spec.parameters)
except SchemaError as exc:
except (SchemaReferenceError, SchemaError) as exc:
message = exc.message if isinstance(exc, SchemaError) else str(exc)
raise ExtensionError(
"PLUGIN_COMMAND_INVALID",
f"Plugin command parameters contain invalid JSON Schema: {exc.message}",
f"Plugin command parameters contain invalid JSON Schema: {message}",
) from exc
@@ -746,7 +760,8 @@ def _secret_field(
def _secret_reference(plugin_id: str, key: str) -> str:
return f"plugin.{plugin_id}.{key}"
digest = hashlib.sha256(f"{plugin_id}\0{key}".encode("utf-8")).hexdigest()
return f"plugin.{digest}"
def _settings_schema_error(plugin_id: str, message: str) -> ExtensionError: