feat(cloud): enforce signed workspace subscription resource limits

This commit is contained in:
RockChinQ
2026-10-15 05:30:36 +00:00
parent 502b38130f
commit b6c4e2c395
11 changed files with 97 additions and 13 deletions
@@ -296,7 +296,9 @@ class PluginsRouterGroup(group.RouterGroup):
async def _check_extensions_limit(self, request_context: RequestContext) -> str | None:
"""Check if extensions limit is reached. Returns error response if limit exceeded, None otherwise."""
await self.ap.plugin_connector.require_workspace_context(request_context)
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
from ...service.workspace_limits import workspace_limits
limitation = await workspace_limits(self.ap, request_context.workspace_uuid)
max_extensions = limitation.get('max_extensions', -1)
if max_extensions >= 0:
plugins = await self.ap.plugin_connector.list_plugins()
@@ -129,6 +129,7 @@ class WorkspacesRouterGroup(group.RouterGroup):
membership = quart.g.workspace_membership
workspace = await self.ap.workspace_service.get_workspace(request_context.workspace_uuid)
plan_name: str | None = None
limitation = None
resolver = getattr(self.ap, 'entitlement_resolver', None)
if workspace.source == WorkspaceSource.CLOUD_PROJECTION.value and resolver is not None:
entitlement = await resolver.resolve(
@@ -136,6 +137,9 @@ class WorkspacesRouterGroup(group.RouterGroup):
minimum_revision=request_context.entitlement_revision,
)
plan_name = entitlement.plan_name
from ...service.workspace_limits import LIMIT_KEYS
limitation = {key: entitlement.limit(value) for key, value in LIMIT_KEYS.items()}
if request_context.principal.principal_type == PrincipalType.SUPPORT_ADMIN:
return self.success(
data={
@@ -154,6 +158,7 @@ class WorkspacesRouterGroup(group.RouterGroup):
'permissions': sorted(request_context.workspace.permissions),
'placement_generation': request_context.placement_generation,
'plan_name': plan_name,
'limitation': limitation,
}
)
account = await self.ap.user_service.get_user_by_uuid(request_context.account_uuid)
@@ -170,6 +175,7 @@ class WorkspacesRouterGroup(group.RouterGroup):
'permissions': sorted(request_context.workspace.permissions),
'placement_generation': request_context.placement_generation,
'plan_name': plan_name,
'limitation': limitation,
}
)
+3 -1
View File
@@ -685,7 +685,9 @@ class BotService:
"""Create bot"""
workspace_uuid = require_workspace_uuid(context)
# Check limitation
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
from .workspace_limits import workspace_limits
limitation = await workspace_limits(self.ap, workspace_uuid)
max_bots = limitation.get('max_bots', -1)
if max_bots >= 0:
existing_bots = await self.get_bots(context)
@@ -56,9 +56,9 @@ class KnowledgeService:
require_workspace_uuid(context)
# In new architecture, we delegate entirely to RAGManager which uses plugins.
# Legacy internal KB creation is removed.
limitation = (
getattr(getattr(self.ap, 'instance_config', None), 'data', {}).get('system', {}).get('limitation', {})
)
from .workspace_limits import workspace_limits
limitation = await workspace_limits(self.ap, require_workspace_uuid(context))
max_knowledge_bases = limitation.get('max_knowledge_bases', -1)
if max_knowledge_bases >= 0:
knowledge_bases = await self.ap.rag_mgr.get_all_knowledge_base_details(context)
+3 -1
View File
@@ -212,7 +212,9 @@ class MCPService:
# stdio MCP even though Box Runtime itself remains available.
require_stdio_mcp_enabled(self.ap, server_data)
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
from .workspace_limits import workspace_limits
limitation = await workspace_limits(self.ap, workspace_uuid)
max_extensions = limitation.get('max_extensions', -1)
if max_extensions >= 0:
mcp_count_result = await self.ap.persistence_mgr.execute_async(
+6 -2
View File
@@ -173,7 +173,9 @@ class PipelineService:
if 'config' in pipeline_data:
RunnerConfigResolver.validate_pipeline_config(pipeline_data['config'])
# Check limitation
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
from .workspace_limits import workspace_limits
limitation = await workspace_limits(self.ap, workspace_uuid)
max_pipelines = limitation.get('max_pipelines', -1)
if max_pipelines >= 0:
existing_pipelines = await self.get_pipelines(context)
@@ -290,7 +292,9 @@ class PipelineService:
"""Copy a pipeline with all its configurations"""
workspace_uuid = require_workspace_uuid(context)
# Check limitation
limitation = self.ap.instance_config.data.get('system', {}).get('limitation', {})
from .workspace_limits import workspace_limits
limitation = await workspace_limits(self.ap, workspace_uuid)
max_pipelines = limitation.get('max_pipelines', -1)
if max_pipelines >= 0:
existing_pipelines = await self.get_pipelines(context)
@@ -0,0 +1,20 @@
"""Resolve resource limits at the Workspace service boundary."""
from ....cloud.entitlements import EntitlementUnavailableError
LIMIT_KEYS = {
'max_bots': 'bots.max',
'max_pipelines': 'pipelines.max',
'max_extensions': 'plugins.max',
'max_knowledge_bases': 'knowledge_bases.max',
}
async def workspace_limits(ap, workspace_uuid: str, *, minimum_revision: int = 0) -> dict[str, int]:
if getattr(getattr(ap, 'deployment', None), 'mode', None) != 'cloud':
return dict(ap.instance_config.data.get('system', {}).get('limitation', {}))
resolver = getattr(ap, 'entitlement_resolver', None)
if resolver is None:
raise EntitlementUnavailableError('Workspace entitlement resolver is unavailable')
snapshot = await resolver.resolve(workspace_uuid, minimum_revision=minimum_revision)
return {config_key: snapshot.limit(limit_key) for config_key, limit_key in LIMIT_KEYS.items()}