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

@@ -49,8 +49,8 @@ class UserKeyManager:
"""
def __init__(self) -> None:
# Cache: raw_key -> (key_id, user_id, scopes, cached_at)
self._cache: dict[str, tuple[str, str, str, float]] = {}
# Cache: raw_key -> (key_id, user_id, scopes, site_id, cached_at)
self._cache: dict[str, tuple[str, str, str, str | None, float]] = {}
async def create_key(
self,
@@ -58,6 +58,7 @@ class UserKeyManager:
name: str,
scopes: str = "read write admin",
expires_in_days: int | None = None,
site_id: str | None = None,
) -> dict[str, Any]:
"""Create a new API key for a user.
@@ -66,10 +67,11 @@ class UserKeyManager:
name: Human label (e.g. "Claude Desktop").
scopes: Access scopes (default: "read write admin" for full access).
expires_in_days: Optional expiry in days from now. None = never.
site_id: Optional site UUID to scope key to a single site.
Returns:
Dict with ``key`` (plaintext, shown once), ``key_id``, ``name``,
``scopes``, ``created_at``, ``expires_at``.
``scopes``, ``created_at``, ``expires_at``, ``site_id``.
"""
from core.database import get_database
@@ -89,9 +91,10 @@ class UserKeyManager:
name=name,
scopes=scopes,
expires_at=expires_at,
site_id=site_id,
)
logger.info("Created user API key %s for user %s", row["id"], user_id)
logger.info("Created user API key %s for user %s (site=%s)", row["id"], user_id, site_id)
return {
"key": raw_key, # shown once
"key_id": row["id"],
@@ -99,6 +102,7 @@ class UserKeyManager:
"scopes": row["scopes"],
"created_at": row["created_at"],
"expires_at": row["expires_at"],
"site_id": row.get("site_id"),
}
async def validate_key(self, api_key: str) -> dict[str, Any] | None:
@@ -118,7 +122,7 @@ class UserKeyManager:
# Check cache first
cached = self._cache.get(api_key)
if cached is not None:
key_id, user_id, scopes, cached_at = cached
key_id, user_id, scopes, site_id, cached_at = cached
if time.time() - cached_at < _CACHE_TTL_SECONDS:
# Update usage in background (fire-and-forget via DB)
try:
@@ -130,7 +134,7 @@ class UserKeyManager:
asyncio.create_task(db.update_api_key_usage(key_id))
except Exception:
pass # Non-critical
return {"key_id": key_id, "user_id": user_id, "scopes": scopes}
return {"key_id": key_id, "user_id": user_id, "scopes": scopes, "site_id": site_id}
else:
del self._cache[api_key]
@@ -159,10 +163,12 @@ class UserKeyManager:
await db.update_api_key_usage(row["id"])
# Cache the result
site_id = row.get("site_id")
self._cache[api_key] = (
row["id"],
row["user_id"],
row["scopes"],
site_id,
time.time(),
)
@@ -170,6 +176,7 @@ class UserKeyManager:
"key_id": row["id"],
"user_id": row["user_id"],
"scopes": row["scopes"],
"site_id": site_id,
}
async def list_keys(self, user_id: str) -> list[dict[str, Any]]: