mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-29 13:47:14 +00:00
fix(cloud): render empty skills without sandbox grant
This commit is contained in:
@@ -12,6 +12,7 @@ from urllib.parse import quote, unquote, urlparse
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
from ....cloud.entitlements import EntitlementFeatureUnavailableError
|
||||||
from ....core import app
|
from ....core import app
|
||||||
from ....skill.utils import parse_frontmatter
|
from ....skill.utils import parse_frontmatter
|
||||||
from ....utils import httpclient
|
from ....utils import httpclient
|
||||||
@@ -119,7 +120,13 @@ class SkillService:
|
|||||||
box_service = self._box_service()
|
box_service = self._box_service()
|
||||||
if box_service is None:
|
if box_service is None:
|
||||||
return []
|
return []
|
||||||
return [self._serialize_skill(skill) for skill in await box_service.list_skills(execution_context)]
|
try:
|
||||||
|
skills = await box_service.list_skills(execution_context)
|
||||||
|
except EntitlementFeatureUnavailableError as error:
|
||||||
|
if error.feature == 'managed_sandbox':
|
||||||
|
return []
|
||||||
|
raise
|
||||||
|
return [self._serialize_skill(skill) for skill in skills]
|
||||||
|
|
||||||
async def get_skill(self, context: TenantContext, skill_name: str) -> Optional[dict]:
|
async def get_skill(self, context: TenantContext, skill_name: str) -> Optional[dict]:
|
||||||
execution_context = await self._execution_context(context)
|
execution_context = await self._execution_context(context)
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ class EntitlementUnavailableError(RuntimeError):
|
|||||||
self.entitlement_revision = entitlement_revision
|
self.entitlement_revision = entitlement_revision
|
||||||
|
|
||||||
|
|
||||||
|
class EntitlementFeatureUnavailableError(EntitlementUnavailableError):
|
||||||
|
"""Raised when an active entitlement explicitly omits a capability."""
|
||||||
|
|
||||||
|
def __init__(self, message: str, *, feature: str, entitlement_revision: int | None = None) -> None:
|
||||||
|
super().__init__(message, entitlement_revision=entitlement_revision)
|
||||||
|
self.feature = feature
|
||||||
|
|
||||||
|
|
||||||
class EntitlementSnapshot(pydantic.BaseModel):
|
class EntitlementSnapshot(pydantic.BaseModel):
|
||||||
"""Capability projection consumed by open-source Core.
|
"""Capability projection consumed by open-source Core.
|
||||||
|
|
||||||
@@ -82,7 +90,11 @@ class EntitlementSnapshot(pydantic.BaseModel):
|
|||||||
|
|
||||||
def require_feature(self, feature: str) -> None:
|
def require_feature(self, feature: str) -> None:
|
||||||
if self.features.get(feature) is not True:
|
if self.features.get(feature) is not True:
|
||||||
raise EntitlementUnavailableError(f'Workspace entitlement does not grant {feature}')
|
raise EntitlementFeatureUnavailableError(
|
||||||
|
f'Workspace entitlement does not grant {feature}',
|
||||||
|
feature=feature,
|
||||||
|
entitlement_revision=self.entitlement_revision,
|
||||||
|
)
|
||||||
|
|
||||||
def limit(self, name: str) -> int:
|
def limit(self, name: str) -> int:
|
||||||
value = self.limits.get(name)
|
value = self.limits.get(name)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import pytest
|
|||||||
|
|
||||||
from langbot.pkg.api.http.context import ExecutionContext
|
from langbot.pkg.api.http.context import ExecutionContext
|
||||||
from langbot.pkg.api.http.service.skill import SkillService
|
from langbot.pkg.api.http.service.skill import SkillService
|
||||||
|
from langbot.pkg.cloud.entitlements import EntitlementFeatureUnavailableError, EntitlementUnavailableError
|
||||||
|
|
||||||
|
|
||||||
_CONTEXT = ExecutionContext(
|
_CONTEXT = ExecutionContext(
|
||||||
@@ -113,6 +114,42 @@ class TestRequireBoxForWrite:
|
|||||||
service = SkillService(self._ap_with_disabled_box())
|
service = SkillService(self._ap_with_disabled_box())
|
||||||
assert await service.list_skills(_CONTEXT) == []
|
assert await service.list_skills(_CONTEXT) == []
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_skills_returns_empty_when_managed_sandbox_is_not_granted(self):
|
||||||
|
box_service = SimpleNamespace(
|
||||||
|
available=True,
|
||||||
|
list_skills=AsyncMock(
|
||||||
|
side_effect=EntitlementFeatureUnavailableError(
|
||||||
|
'Workspace entitlement does not grant managed_sandbox',
|
||||||
|
feature='managed_sandbox',
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
service = SkillService(
|
||||||
|
SimpleNamespace(
|
||||||
|
workspace_service=_workspace_service(),
|
||||||
|
box_service=box_service,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await service.list_skills(_CONTEXT) == []
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_skills_preserves_other_entitlement_failures(self):
|
||||||
|
box_service = SimpleNamespace(
|
||||||
|
available=True,
|
||||||
|
list_skills=AsyncMock(side_effect=EntitlementUnavailableError('control plane unavailable')),
|
||||||
|
)
|
||||||
|
service = SkillService(
|
||||||
|
SimpleNamespace(
|
||||||
|
workspace_service=_workspace_service(),
|
||||||
|
box_service=box_service,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(EntitlementUnavailableError, match='control plane unavailable'):
|
||||||
|
await service.list_skills(_CONTEXT)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_read_skill_file_refused_when_box_unavailable(self):
|
async def test_read_skill_file_refused_when_box_unavailable(self):
|
||||||
service = SkillService(self._ap_with_disabled_box())
|
service = SkillService(self._ap_with_disabled_box())
|
||||||
|
|||||||
Reference in New Issue
Block a user