Skip to content

Commit 24c43a5

Browse files
feat: add MCP server allowlist/blocklist and plugin trust tiers (#425, #426)
MCP Server Allowlist/Blocklist (#425): - MarketplacePolicy model with MCPServerPolicy (allowlist/blocklist mode) - evaluate_plugin_compliance() checks manifests against policy - PluginRegistry enforces policy on register() — rejects non-compliant plugins - CLI: agent-marketplace evaluate --marketplace-policy <path> --manifest <path> - load_marketplace_policy() for YAML policy files - 35 new tests Plugin Trust Tiers (#426): - 5-tier trust model (revoked/probationary/standard/trusted/verified, 0-1000) - PluginTrustConfig with per-tier capability limits (token budget, tool calls, access level) - PluginTrustStore — file-backed JSON persistence with event recording - compute_initial_score() based on manifest quality (signature, capabilities) - filter_capabilities() gates capabilities by trust tier - CLI: agent-marketplace trust <plugin-name> - 43 new tests Total: 135 tests passing. Closes #425 Closes #426 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2388153 commit 24c43a5

File tree

7 files changed

+1540
-3
lines changed

7 files changed

+1540
-3
lines changed

packages/agent-marketplace/src/agent_marketplace/__init__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
load_manifest,
1717
save_manifest,
1818
)
19+
from agent_marketplace.marketplace_policy import (
20+
ComplianceResult,
21+
MCPServerPolicy,
22+
MarketplacePolicy,
23+
evaluate_plugin_compliance,
24+
load_marketplace_policy,
25+
)
1926
from agent_marketplace.registry import PluginRegistry
2027
from agent_marketplace.schema_adapters import (
2128
ClaudePluginManifest,
@@ -26,22 +33,45 @@
2633
extract_mcp_servers,
2734
)
2835
from agent_marketplace.signing import PluginSigner, verify_signature
36+
from agent_marketplace.trust_tiers import (
37+
DEFAULT_TIER_CONFIGS,
38+
TRUST_TIERS,
39+
PluginTrustConfig,
40+
PluginTrustStore,
41+
compute_initial_score,
42+
filter_capabilities,
43+
get_tier_config,
44+
get_trust_tier,
45+
)
2946

3047
__all__ = [
3148
"ClaudePluginManifest",
49+
"ComplianceResult",
3250
"CopilotPluginManifest",
51+
"DEFAULT_TIER_CONFIGS",
3352
"MANIFEST_FILENAME",
53+
"MCPServerPolicy",
3454
"MarketplaceError",
55+
"MarketplacePolicy",
3556
"PluginInstaller",
3657
"PluginManifest",
3758
"PluginRegistry",
3859
"PluginSigner",
60+
"PluginTrustConfig",
61+
"PluginTrustStore",
3962
"PluginType",
63+
"TRUST_TIERS",
4064
"adapt_to_canonical",
65+
"compute_initial_score",
4166
"detect_manifest_format",
67+
"evaluate_plugin_compliance",
4268
"extract_capabilities",
4369
"extract_mcp_servers",
70+
"filter_capabilities",
71+
"get_tier_config",
72+
"get_trust_tier",
4473
"load_manifest",
74+
"load_marketplace_policy",
4575
"save_manifest",
4676
"verify_signature",
4777
]

packages/agent-marketplace/src/agent_marketplace/cli_commands.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131
PluginType,
3232
load_manifest,
3333
)
34+
from agent_marketplace.marketplace_policy import (
35+
ComplianceResult,
36+
MarketplacePolicy,
37+
MCPServerPolicy,
38+
evaluate_plugin_compliance,
39+
load_marketplace_policy,
40+
)
3441
from agent_marketplace.schema_adapters import (
3542
adapt_to_canonical,
3643
detect_manifest_format,
@@ -224,6 +231,116 @@ def publish_plugin(path: str) -> None:
224231
console.print(f"[red]Error:[/red] {exc}")
225232

226233

234+
@plugin.command("evaluate")
235+
@click.argument("manifest_path", metavar="MANIFEST", type=click.Path(exists=True))
236+
@click.option(
237+
"--marketplace-policy",
238+
required=True,
239+
type=click.Path(exists=True),
240+
help="Path to marketplace policy YAML file",
241+
)
242+
def evaluate_plugin(manifest_path: str, marketplace_policy: str) -> None:
243+
"""Evaluate a single plugin manifest against a marketplace policy.
244+
245+
Loads the MANIFEST (agent-plugin.yaml or plugin.json) and checks it
246+
against the marketplace policy. MCP server names are extracted
247+
automatically when the manifest declares them.
248+
249+
Exit code 0 if compliant, 1 if any violations exist.
250+
"""
251+
import json as _json
252+
import sys
253+
254+
policy_path = Path(marketplace_policy)
255+
mpath = Path(manifest_path)
256+
257+
try:
258+
policy = load_marketplace_policy(policy_path)
259+
except MarketplaceError as exc:
260+
console.print(f"[red]Error:[/red] {exc}")
261+
sys.exit(1)
262+
263+
try:
264+
# Load raw data for MCP server extraction
265+
target = mpath
266+
if target.is_dir():
267+
json_path = target / "plugin.json"
268+
yaml_path = target / "agent-plugin.yaml"
269+
if json_path.exists():
270+
target = json_path
271+
elif yaml_path.exists():
272+
target = yaml_path
273+
else:
274+
raise MarketplaceError(f"No manifest found in {target}")
275+
276+
text = target.read_text(encoding="utf-8")
277+
if target.suffix == ".json":
278+
raw_data = _json.loads(text)
279+
else:
280+
import yaml
281+
282+
raw_data = yaml.safe_load(text)
283+
284+
fmt = detect_manifest_format(raw_data)
285+
if fmt == "generic":
286+
manifest = load_manifest(mpath)
287+
else:
288+
manifest = adapt_to_canonical(raw_data, fmt)
289+
290+
mcp_servers = extract_mcp_servers(raw_data)
291+
except MarketplaceError as exc:
292+
console.print(f"[red]Error:[/red] {exc}")
293+
sys.exit(1)
294+
295+
result = evaluate_plugin_compliance(
296+
manifest, policy, mcp_servers or None
297+
)
298+
299+
if result.compliant:
300+
console.print(
301+
f"[green]✓[/green] Plugin '{manifest.name}' is compliant"
302+
)
303+
else:
304+
console.print(
305+
f"[red]✗[/red] Plugin '{manifest.name}' has policy violations:"
306+
)
307+
for violation in result.violations:
308+
console.print(f" - {violation}")
309+
sys.exit(1)
310+
311+
312+
@plugin.command("trust")
313+
@click.argument("plugin_name", metavar="PLUGIN_NAME")
314+
@click.option(
315+
"--store",
316+
"store_path",
317+
default=str(Path(".agentmesh") / "trust.json"),
318+
help="Path to the trust store JSON file",
319+
)
320+
def trust_plugin(plugin_name: str, store_path: str) -> None:
321+
"""Show trust score and tier for a plugin."""
322+
from agent_marketplace.trust_tiers import (
323+
PluginTrustStore,
324+
get_tier_config,
325+
get_trust_tier,
326+
)
327+
328+
store = PluginTrustStore(store_path=Path(store_path))
329+
score = store.get_score(plugin_name)
330+
tier = get_trust_tier(score)
331+
config = get_tier_config(tier)
332+
333+
table = Table(title=f"Trust: {plugin_name}")
334+
table.add_column("Property", style="cyan")
335+
table.add_column("Value")
336+
table.add_row("Score", str(score))
337+
table.add_row("Tier", tier)
338+
table.add_row("Max Token Budget", str(config.max_token_budget))
339+
table.add_row("Max Tool Calls", str(config.max_tool_calls))
340+
table.add_row("Tool Access", config.allowed_tool_access)
341+
console.print(table)
342+
343+
227344
# Register batch evaluation command
228345
try:
229346
from agent_marketplace.batch import evaluate_batch_command
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
"""
4+
Marketplace Policy Enforcement
5+
6+
Defines marketplace-level policies for MCP server allowlist/blocklist
7+
enforcement, plugin type restrictions, and signature requirements.
8+
Operators can declare which MCP servers are permitted for plugins.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import logging
14+
from pathlib import Path
15+
from typing import Optional
16+
17+
import yaml
18+
from pydantic import BaseModel, Field, field_validator
19+
20+
from agent_marketplace.exceptions import MarketplaceError
21+
from agent_marketplace.manifest import PluginManifest
22+
23+
logger = logging.getLogger(__name__)
24+
25+
26+
# ---------------------------------------------------------------------------
27+
# Policy models
28+
# ---------------------------------------------------------------------------
29+
30+
31+
class MCPServerPolicy(BaseModel):
32+
"""Controls which MCP servers plugins are allowed to use."""
33+
34+
mode: str = Field(
35+
"allowlist",
36+
description="Enforcement mode: 'allowlist' or 'blocklist'",
37+
)
38+
allowed: list[str] = Field(
39+
default_factory=list,
40+
description="Allowed MCP server names (when mode=allowlist)",
41+
)
42+
blocked: list[str] = Field(
43+
default_factory=list,
44+
description="Blocked MCP server names (when mode=blocklist)",
45+
)
46+
require_declaration: bool = Field(
47+
False,
48+
description="Plugins must declare all MCP servers they use",
49+
)
50+
51+
@field_validator("mode")
52+
@classmethod
53+
def validate_mode(cls, v: str) -> str:
54+
if v not in ("allowlist", "blocklist"):
55+
raise MarketplaceError(
56+
f"Invalid MCP server policy mode: {v} (expected 'allowlist' or 'blocklist')"
57+
)
58+
return v
59+
60+
61+
class MarketplacePolicy(BaseModel):
62+
"""Top-level marketplace policy controlling plugin admission."""
63+
64+
mcp_servers: MCPServerPolicy = Field(
65+
default_factory=MCPServerPolicy,
66+
description="MCP server allowlist/blocklist policy",
67+
)
68+
allowed_plugin_types: Optional[list[str]] = Field(
69+
None,
70+
description="Restrict which plugin types may be registered",
71+
)
72+
require_signature: bool = Field(
73+
False,
74+
description="Require Ed25519 signatures on all plugins",
75+
)
76+
77+
78+
class ComplianceResult(BaseModel):
79+
"""Result of evaluating a plugin against a marketplace policy."""
80+
81+
compliant: bool = Field(..., description="Whether the plugin is compliant")
82+
violations: list[str] = Field(
83+
default_factory=list,
84+
description="Human-readable violation descriptions",
85+
)
86+
87+
88+
# ---------------------------------------------------------------------------
89+
# Policy loading
90+
# ---------------------------------------------------------------------------
91+
92+
93+
def load_marketplace_policy(path: Path) -> MarketplacePolicy:
94+
"""Load a marketplace policy from a YAML file.
95+
96+
Args:
97+
path: Path to the policy YAML file.
98+
99+
Returns:
100+
Parsed MarketplacePolicy.
101+
102+
Raises:
103+
MarketplaceError: If the file is missing or invalid.
104+
"""
105+
if not path.exists():
106+
raise MarketplaceError(f"Marketplace policy file not found: {path}")
107+
try:
108+
with open(path) as f:
109+
data = yaml.safe_load(f)
110+
if not isinstance(data, dict):
111+
raise MarketplaceError("Marketplace policy must be a YAML mapping")
112+
return MarketplacePolicy(**data)
113+
except MarketplaceError:
114+
raise
115+
except Exception as exc:
116+
raise MarketplaceError(f"Failed to load marketplace policy: {exc}") from exc
117+
118+
119+
# ---------------------------------------------------------------------------
120+
# Compliance evaluation
121+
# ---------------------------------------------------------------------------
122+
123+
124+
def evaluate_plugin_compliance(
125+
manifest: PluginManifest,
126+
policy: MarketplacePolicy,
127+
mcp_servers: list[str] | None = None,
128+
) -> ComplianceResult:
129+
"""Check whether a plugin manifest complies with a marketplace policy.
130+
131+
Args:
132+
manifest: The plugin manifest to evaluate.
133+
policy: The marketplace policy to enforce.
134+
mcp_servers: Optional list of MCP server names declared by the plugin.
135+
When ``None``, MCP declaration checks that require a server list
136+
will flag a violation if ``require_declaration`` is enabled.
137+
138+
Returns:
139+
A :class:`ComplianceResult` indicating compliance status and any
140+
violations.
141+
"""
142+
violations: list[str] = []
143+
144+
# -- Signature requirement ------------------------------------------------
145+
if policy.require_signature and not manifest.signature:
146+
violations.append(
147+
f"Plugin '{manifest.name}' must be signed (Ed25519 signature required)"
148+
)
149+
150+
# -- Plugin type restriction ----------------------------------------------
151+
if policy.allowed_plugin_types is not None:
152+
if manifest.plugin_type.value not in policy.allowed_plugin_types:
153+
violations.append(
154+
f"Plugin type '{manifest.plugin_type.value}' is not allowed "
155+
f"(allowed: {', '.join(policy.allowed_plugin_types)})"
156+
)
157+
158+
# -- MCP server policy ----------------------------------------------------
159+
mcp_policy = policy.mcp_servers
160+
161+
if mcp_policy.require_declaration and mcp_servers is None:
162+
violations.append(
163+
f"Plugin '{manifest.name}' must declare its MCP servers"
164+
)
165+
166+
if mcp_servers is not None:
167+
if mcp_policy.mode == "allowlist" and mcp_policy.allowed:
168+
disallowed = [s for s in mcp_servers if s not in mcp_policy.allowed]
169+
if disallowed:
170+
violations.append(
171+
f"MCP servers not in allowlist: {', '.join(disallowed)}"
172+
)
173+
174+
if mcp_policy.mode == "blocklist" and mcp_policy.blocked:
175+
blocked_found = [s for s in mcp_servers if s in mcp_policy.blocked]
176+
if blocked_found:
177+
violations.append(
178+
f"MCP servers are blocked: {', '.join(blocked_found)}"
179+
)
180+
181+
return ComplianceResult(
182+
compliant=len(violations) == 0,
183+
violations=violations,
184+
)

0 commit comments

Comments
 (0)