Publish curated Unraid-AppOps-Gateway source from 28831bd43867347a132f1c2b0f388632b9cb561e

This commit is contained in:
Public Source Publisher committed 2026-10-09 13:09:48 +00:00
1 parent 41fd48f0bc
commit 2d50064a8f
4 files changed
+92 -7

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
# Public source export # Public source export
This source snapshot was generated from the private canonical repository at revision `6d175c3333218e2b580a310496fed6c407f28114`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. This source snapshot was generated from the private canonical repository at revision `28831bd43867347a132f1c2b0f388632b9cb561e`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"sourceRevision": "6d175c3333218e2b580a310496fed6c407f28114", "sourceRevision": "28831bd43867347a132f1c2b0f388632b9cb561e",
"files": [ "files": [
{ {
"path": ".dockerignore", "path": ".dockerignore",
@@ -404,8 +404,8 @@
}, },
{ {
"path": "backend/app/mcp_server.py", "path": "backend/app/mcp_server.py",
"sha256": "598e7f7a5020d92becfb143d31dbd096f16bf79ae73b26e6ecae710661f73ca1", "sha256": "065bc0c1f9779f0df30cb6d19eee129b0cf3c4b66a190687e9bfc682c239c94a",
"bytes": 50519 "bytes": 51192
}, },
{ {
"path": "backend/app/runner_statistics_mcp.py", "path": "backend/app/runner_statistics_mcp.py",
@@ -1284,8 +1284,8 @@
}, },
{ {
"path": "backend/tests/test_mcp_rag_n8n_integrations.py", "path": "backend/tests/test_mcp_rag_n8n_integrations.py",
"sha256": "1d72e95529863d3263a178d71d4c7729c8b624b13e734460fb9303a3f6f7416d", "sha256": "587ff5dac2861f47224f0ec174ad0ef2472980f2ba41d28d7f2d72eb4015abd7",
"bytes": 54507 "bytes": 57461
}, },
{ {
"path": "backend/tests/test_media_codec_dashboard_deployment_reconciliation.py", "path": "backend/tests/test_media_codec_dashboard_deployment_reconciliation.py",
+14 -1
View File
@@ -13,6 +13,7 @@ from time import perf_counter
from typing import Any from typing import Any
from mcp.server import MCPServer from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.server.transport_security import TransportSecuritySettings from mcp.server.transport_security import TransportSecuritySettings
from mcp.types import ToolAnnotations from mcp.types import ToolAnnotations
from pydantic import StrictBool, ValidationError from pydantic import StrictBool, ValidationError
@@ -215,7 +216,19 @@ def run_read_only_canary() -> dict[str, Any]:
return dict(_CANARY_STATUS) return dict(_CANARY_STATUS)
appops_mcp = MCPServer( class _InputRedactingMCPServer(MCPServer):
async def call_tool(self, name: str, arguments: dict[str, Any], context: Any = None) -> Any:
try:
return await super().call_tool(name, arguments, context)
except ToolError as error:
# The SDK wraps argument validation before calling our typed handlers.
# Do not let its input-bearing error text or cause reach the wire/logger.
if type(error) is ToolError and isinstance(error.__cause__, ValidationError):
raise ToolError("invalid_tool_arguments") from None
raise
appops_mcp = _InputRedactingMCPServer(
"Unraid AppOps Gateway", "Unraid AppOps Gateway",
version=APP_VERSION, version=APP_VERSION,
instructions=( instructions=(
@@ -6,6 +6,7 @@ import json
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import pytest import pytest
from pydantic import StrictBool
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from mcp import Client from mcp import Client
from sqlmodel import Session, SQLModel, create_engine, select from sqlmodel import Session, SQLModel, create_engine, select
@@ -1410,3 +1411,74 @@ def test_mcp_typed_schema_rejection_never_echoes_opaque_input(
asyncio.run(exercise()) asyncio.run(exercise())
assert mcp_server._MCP_EXECUTION_LOCK.locked() is False assert mcp_server._MCP_EXECUTION_LOCK.locked() is False
@pytest.mark.parametrize(("name", "argument", "shape"), [
(name, argument, shape)
for name, argument in [
("enrollment.preflight", "plan"),
("target.enroll", "plan"),
("deployment.preflight", "plan"),
("deployment.apply", "plan"),
("deployment.rollback", "plan"),
("forge.bootstrap.apply", "profile"),
]
for shape in ["string", "list", "idempotency"]
if shape != "idempotency" or name != "enrollment.preflight"
])
def test_mcp_sdk_argument_rejection_never_echoes_input_or_logs(
integration_client, monkeypatch, caplog, name, argument, shape,
):
from backend.app import mcp_server
canary = "syntheticSdkOpaqueInput_6Q8XZ"
arguments = {argument: [canary] if shape == "list" else canary}
if shape == "idempotency":
arguments = {argument: {}, "idempotencyKey": [canary]}
elif name != "enrollment.preflight":
arguments["idempotencyKey"] = "schema-redaction-fixture"
def forbidden_service(*_args, **_kwargs):
pytest.fail("invalid SDK arguments must not reach execution services")
monkeypatch.setattr(mcp_server, "EnrollmentPlane", forbidden_service)
monkeypatch.setattr(mcp_server, "DeploymentControlPlane", forbidden_service)
caplog.set_level("INFO")
async def exercise():
async with Client(mcp_server.appops_mcp) as client:
result = await client.call_tool(name, arguments)
assert result.is_error is True
assert [item.text for item in result.content] == ["invalid_tool_arguments"]
assert canary not in json.dumps(result.model_dump(mode="json"))
asyncio.run(exercise())
assert canary not in caplog.text
assert mcp_server._MCP_EXECUTION_LOCK.locked() is False
def test_mcp_sdk_redaction_preserves_schema_valid_calls_and_domain_denials():
from backend.app.mcp_server import _InputRedactingMCPServer
from mcp.server.mcpserver.exceptions import ToolError
server = _InputRedactingMCPServer("redaction-compatibility-fixture")
calls = []
@server.tool(name="governed.fixture")
def governed(allowed: StrictBool) -> dict[str, str]:
calls.append(allowed)
if not allowed:
raise ToolError("policy_denied")
return {"status": "allowed"}
async def exercise():
async with Client(server) as client:
listing = await client.list_tools()
assert listing.tools[0].input_schema["properties"]["allowed"]["type"] == "boolean"
accepted = await client.call_tool("governed.fixture", {"allowed": True})
assert accepted.is_error is False
assert accepted.structured_content == {"status": "allowed"}
denied = await client.call_tool("governed.fixture", {"allowed": False})
assert denied.is_error is True
assert "policy_denied" in denied.content[0].text
asyncio.run(exercise())
assert calls == [True, False]