feat(F.7+F.17): v3.11.0 — Coolify plugin (67 tools) + tool access overhaul
Some checks failed
Release / Test before release (push) Has been cancelled
Release / Publish to PyPI (push) Has been cancelled
Release / Publish to Docker Hub (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled

Catch-up sync spanning v3.7.0 → v3.11.0 of the internal repo.

Platform
- Total tools: 565 → 633 (+68) across 10 plugins (Coolify added)
- Tests: 481 → 828 passing

New plugin: Coolify (67 tools, Track F.17)
- Applications (17): CRUD, lifecycle, logs, env vars
- Deployments (5): list/get/cancel/deploy, app history
- Servers (8): CRUD, resources, domains, validation
- Projects (8), Databases (16, 6 DB types + backups), Services (13)

Tool access system (Track F.7 → F.7d)
- Scope → category mapping with per-tool `category` + `sensitivity`
- Schema v7: `site_tool_toggles(site_id)` + `sites.tool_scope` column
- Schema v8: per-site API keys (`api_keys.site_id`)
- Plugin-specific access-level presets (WP / WC / Gitea / OpenPanel /
  Coolify 5-tier)
- Credential-requirement notice tailored per plugin and tier
- Admin Tools count card on service page
- Dropped redundant `write` tier on WP / WP Advanced / WooCommerce
  (admin-scope tool count = 0 → identical to admin tier)

Dashboard
- Unified site manage page (Connection / Tool Access / Connect)
- /dashboard/keys unified (was /api-keys and /connect)
- CSRF interceptor via meta-tag; removed conflicting cookie reader
- Tailwind: pre-built CSS (scripts/build-css.sh) replaces CDN

Docs
- README / DOCKER_README / CLAUDE updated to 633 tools / 10 plugins
- CHANGELOG entries for v3.7.0 → v3.11.0
- FastMCP compatibility note updated to 3.x (post-v3.5 upgrade)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 21:05:07 +02:00
parent d8a0805412
commit 788439e377
31 changed files with 1911 additions and 254 deletions

View File

@@ -1,23 +1,17 @@
"""Tool access manager — site-scoped visibility and per-site toggles (F.7b).
"""Tool access manager — site-scoped visibility and per-site toggles (F.7c).
Provides a central pipeline that filters the set of MCP tools presented for
a user endpoint based on:
1. **Scope → category mapping.** Every ``ToolDefinition`` carries a
``category`` field (e.g. ``read``, ``lifecycle``, ``crud``, ``system``).
An API key's declared scopes **and** the site's stored ``tool_scope``
preset each map to a set of allowed categories via
:data:`SCOPE_TO_CATEGORIES`. A tool is visible only if its category is in
the intersection — the narrower of the two layers wins.
1. **Universal scope tiers.** A 3-level system (``read`` / ``write`` /
``admin``) that works across ALL plugins using the tool's
``required_scope`` field. For Coolify (which has fine-grained
``category`` annotations) the legacy category mapping is kept as an
overlay for the ``custom`` preset.
2. **Per-site tool toggles.** Site owners may explicitly disable specific
tools via the ``site_tool_toggles`` table. Only overrides are stored —
tools via the ``site_tool_toggles`` table. Only overrides are stored —
tools without an entry are enabled by default.
Tools whose ``category`` is not in :data:`KNOWN_CATEGORIES` are **always
visible** (backward compatibility — legacy plugins that have not been
annotated yet default to ``category="read"``, which belongs to the ``read``
scope set anyway, but an unknown value would be preserved).
The ``tool_scope`` value ``"custom"`` is a sentinel meaning "do not apply a
site-level preset filter" — in that case only the per-tool toggles and the
key scope are considered.
@@ -44,9 +38,17 @@ from core.tool_registry import ToolDefinition
logger = logging.getLogger(__name__)
# ── Universal 3-tier scope system (F.7c) ─────────────────────────────
# Maps a scope tier to the set of ``required_scope`` values it may access.
# Works for ALL plugins because every tool has ``required_scope``.
UNIVERSAL_SCOPE_TIERS: dict[str, set[str]] = {
"read": {"read"},
"write": {"read", "write"},
"admin": {"read", "write", "admin"},
}
# ── Legacy Coolify category mapping (kept for ``custom`` overlay) ─────
# Mapping from scope → set of tool categories that scope may see.
# Used for BOTH API-key scopes and per-site ``tool_scope`` presets.
# Scopes are additive: presenting multiple scopes yields the union.
SCOPE_TO_CATEGORIES: dict[str, set[str]] = {
"read": {"read"},
"read:sensitive": {"read", "read_sensitive", "backup"},
@@ -63,8 +65,7 @@ SCOPE_TO_CATEGORIES: dict[str, set[str]] = {
},
}
# All known categories — any tool whose category is outside this set is
# treated as "always visible" for backward compatibility.
# All known Coolify categories.
KNOWN_CATEGORIES: set[str] = {
"read",
"read_sensitive",
@@ -78,6 +79,9 @@ KNOWN_CATEGORIES: set[str] = {
# Sentinel meaning "no site-level preset filter — use per-tool toggles only".
SCOPE_CUSTOM = "custom"
# Plugins that have fine-grained category annotations.
_CATEGORY_PLUGINS: set[str] = {"coolify"}
def scopes_to_categories(scopes: list[str]) -> set[str]:
"""Return the union of categories allowed by the given scope list.
@@ -94,6 +98,197 @@ def scopes_to_categories(scopes: list[str]) -> set[str]:
return allowed
def _scopes_to_required(scopes: list[str]) -> set[str]:
"""Return the union of ``required_scope`` values allowed by universal tiers."""
allowed: set[str] = set()
for scope in scopes:
allowed |= UNIVERSAL_SCOPE_TIERS.get(scope.strip(), set())
return allowed
def get_scope_presets_for_plugin(plugin_type: str) -> list[dict[str, str]]:
"""Return the appropriate scope presets for a plugin type (F.7d).
Each preset is a dict with ``value`` (the canonical scope key persisted
to ``sites.tool_scope``), ``label`` / ``label_fa`` (button title), and
``hint`` / ``hint_fa`` (one-line description shown under the title).
The valid scope values are constrained by ``_VALID_TOOL_SCOPES`` in
``core.dashboard.routes`` — currently:
``{"read", "read:sensitive", "deploy", "write", "admin", "custom"}``.
"""
custom = {
"value": "custom",
"label": "Custom",
"label_fa": "سفارشی",
"hint": "Per-tool toggles",
"hint_fa": "هر ابزار جداگانه",
}
if plugin_type == "coolify":
# 5 fine-grained Coolify tiers + custom (matches SCOPE_TO_CATEGORIES).
return [
{
"value": "read",
"label": "Read",
"label_fa": "خواندن",
"hint": "List/inspect resources",
"hint_fa": "مشاهده و فهرست منابع",
},
{
"value": "read:sensitive",
"label": "Read + Secrets",
"label_fa": "خواندن + اسرار",
"hint": "Includes env vars and backups",
"hint_fa": "شامل متغیرهای محیطی و بکاپ",
},
{
"value": "deploy",
"label": "Deploy",
"label_fa": "استقرار",
"hint": "Read + lifecycle (start/stop/restart)",
"hint_fa": "مشاهده + راه‌اندازی/توقف/ریستارت",
},
{
"value": "write",
"label": "Write",
"label_fa": "نوشتن",
"hint": "Read + lifecycle + CRUD + env",
"hint_fa": "مشاهده + لایفسایکل + CRUD + env",
},
{
"value": "admin",
"label": "Root",
"label_fa": "روت",
"hint": "Everything including system commands",
"hint_fa": "همه چیز شامل دستورات سیستم",
},
custom,
]
if plugin_type == "openpanel":
return [
{
"value": "read",
"label": "Read",
"label_fa": "خواندن",
"hint": "Export current project only",
"hint_fa": "خروجی پروژه فعلی",
},
{
"value": "write",
"label": "Write",
"label_fa": "نوشتن",
"hint": "Default ingestion (track events)",
"hint_fa": "ارسال رویداد (ingestion)",
},
{
"value": "admin",
"label": "Root",
"label_fa": "روت",
"hint": "Export any project",
"hint_fa": "خروجی هر پروژه",
},
custom,
]
if plugin_type == "woocommerce":
# WooCommerce has no admin-scope tools (read=14, write=14, admin=0),
# so Write and "Read + Write" tiers are identical. Present a single
# full-access tier instead of two duplicates.
return [
{
"value": "read",
"label": "Read Only",
"label_fa": "فقط خواندن",
"hint": "Browse products, orders, customers",
"hint_fa": "مشاهده محصولات، سفارش‌ها و مشتریان",
},
{
"value": "admin",
"label": "Read + Write",
"label_fa": "خواندن + نوشتن",
"hint": "Full store management (all 28 tools)",
"hint_fa": "مدیریت کامل فروشگاه (همه ۲۸ ابزار)",
},
custom,
]
if plugin_type in {"wordpress", "wordpress_advanced"}:
# WordPress has no admin-scope tools (read=27, write=40, admin=0).
# SEO + plugin/theme tools require the Airano MCP SEO Bridge plugin
# to be installed on the WP site itself. Present 2 tiers + custom.
return [
{
"value": "read",
"label": "Read Only",
"label_fa": "فقط خواندن",
"hint": "View posts, pages, media",
"hint_fa": "مشاهده نوشته‌ها، صفحات و رسانه",
},
{
"value": "admin",
"label": "Full Access",
"label_fa": "دسترسی کامل",
"hint": "All tools (CRUD + SEO via add-on)",
"hint_fa": "همه ابزارها (CRUD و SEO با افزونه)",
},
custom,
]
if plugin_type == "gitea":
return [
{
"value": "read",
"label": "Read",
"label_fa": "خواندن",
"hint": "Browse repos, issues, users",
"hint_fa": "مشاهده مخازن، ایشوها، کاربران",
},
{
"value": "write",
"label": "Read + Write",
"label_fa": "خواندن + نوشتن",
"hint": "Create issues, PRs, branches",
"hint_fa": "ایجاد ایشو، PR و شاخه",
},
{
"value": "admin",
"label": "Admin",
"label_fa": "مدیر",
"hint": "Repo + org + user admin",
"hint_fa": "مدیریت مخزن، سازمان و کاربر",
},
custom,
]
# Universal default for all other plugins.
return [
{
"value": "read",
"label": "Read",
"label_fa": "فقط خواندن",
"hint": "View only",
"hint_fa": "فقط مشاهده",
},
{
"value": "write",
"label": "Read + Write",
"label_fa": "خواندن + نوشتن",
"hint": "CRUD ops",
"hint_fa": "عملیات CRUD",
},
{
"value": "admin",
"label": "Full Access",
"label_fa": "دسترسی کامل",
"hint": "All tools",
"hint_fa": "همه ابزارها",
},
custom,
]
class ToolAccessManager:
"""Central manager for scope-based visibility and per-site tool toggles."""
@@ -101,24 +296,35 @@ class ToolAccessManager:
self,
tools: list[ToolDefinition],
scopes: list[str],
plugin_type: str | None = None,
) -> list[ToolDefinition]:
"""Drop tools whose category is not allowed by the presented scopes.
"""Drop tools not allowed by the presented scopes.
Tools with an unknown category (e.g. legacy plugins not yet annotated)
are always kept — backward compatibility.
For plugins with category annotations (Coolify) the legacy
category-based filter is used. For all other plugins the universal
3-tier filter based on ``required_scope`` is applied.
Args:
tools: Candidate tool list.
scopes: Scopes presented on the API key (or a single-element list
containing a site's ``tool_scope`` preset).
plugin_type: Plugin type hint. When provided and the plugin is
NOT in ``_CATEGORY_PLUGINS``, the universal tier filter is
used.
Returns:
Filtered tool list.
"""
# Try universal tiers first (works for all plugins)
allowed_scopes = _scopes_to_required(scopes)
if allowed_scopes and (plugin_type is None or plugin_type not in _CATEGORY_PLUGINS):
# Universal filter: match tool.required_scope against allowed tiers
return [t for t in tools if t.required_scope in allowed_scopes]
# Fallback: legacy category-based filter for Coolify / custom scopes
allowed = scopes_to_categories(scopes)
if not allowed:
# No recognised scopes — preserve legacy behaviour and return
# only tools with unknown categories.
return [t for t in tools if t.category not in KNOWN_CATEGORIES]
result: list[ToolDefinition] = []
@@ -185,7 +391,7 @@ class ToolAccessManager:
registry = get_tool_registry()
tools = registry.get_by_plugin_type(plugin_type)
tools = self.apply_scope_filter(tools, key_scopes)
tools = self.apply_scope_filter(tools, key_scopes, plugin_type=plugin_type)
try:
db = get_database()
@@ -194,7 +400,7 @@ class ToolAccessManager:
site_scope = "admin"
if site_scope and site_scope != SCOPE_CUSTOM:
tools = self.apply_scope_filter(tools, [site_scope])
tools = self.apply_scope_filter(tools, [site_scope], plugin_type=plugin_type)
tools = await self.apply_site_toggles(tools, site_id)
return tools