feat(workspace): add in-product collaboration and direct Cloud launch

This commit is contained in:
dadachann
2026-07-26 10:40:36 +08:00
parent 276791e7af
commit 8a445bfb22
29 changed files with 1258 additions and 124 deletions
@@ -64,12 +64,29 @@ class TestSpaceOAuthState:
service = UserService(SimpleNamespace())
state = await service.issue_space_oauth_state('login')
digest = service._space_oauth_state_digest(state)
purpose, account_uuid, _ = service._space_oauth_states[digest]
service._space_oauth_states[digest] = (purpose, account_uuid, 0)
purpose, account_uuid, _, launch_workspace_uuid = service._space_oauth_states[digest]
service._space_oauth_states[digest] = (purpose, account_uuid, 0, launch_workspace_uuid)
with pytest.raises(ValueError, match='Invalid or expired OAuth state'):
await service.consume_space_oauth_state(state, 'login')
async def test_login_state_can_carry_launch_workspace_without_changing_normal_return(self):
service = UserService(SimpleNamespace())
state = await service.issue_space_oauth_state(
'login',
launch_workspace_uuid='workspace-a',
)
assert await service.consume_space_oauth_state(state, 'login') is None
state = await service.issue_space_oauth_state(
'login',
launch_workspace_uuid='workspace-a',
)
consumed = await service.consume_space_oauth_state_details(state, 'login')
assert consumed.account is None
assert consumed.launch_workspace_uuid == 'workspace-a'
def _create_mock_user(
email: str = 'test@example.com',
+120
View File
@@ -0,0 +1,120 @@
from __future__ import annotations
import base64
import json
import time
import uuid
from types import SimpleNamespace
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from langbot.pkg.cloud.launch import SpaceLaunchError, SpaceLaunchService
pytestmark = pytest.mark.asyncio
INSTANCE_UUID = 'instance-test'
ACCOUNT_UUID = '11111111-1111-4111-8111-111111111111'
WORKSPACE_UUID = '22222222-2222-4222-8222-222222222222'
KEY_ID = 'space-key-1'
def _base64url(raw: bytes) -> str:
return base64.urlsafe_b64encode(raw).rstrip(b'=').decode('ascii')
def _sign(private_key: Ed25519PrivateKey, claims: dict, *, key_id: str = KEY_ID) -> str:
header = {'alg': 'EdDSA', 'kid': key_id, 'typ': 'langbot-control-plane+jwt'}
encoded_header = _base64url(json.dumps(header, separators=(',', ':')).encode('utf-8'))
encoded_claims = _base64url(json.dumps(claims, separators=(',', ':')).encode('utf-8'))
signing_input = f'{encoded_header}.{encoded_claims}'
return f'{signing_input}.{_base64url(private_key.sign(signing_input.encode("ascii")))}'
def _claims(*, now: int, jti: str | None = None, workspace_uuid: str = WORKSPACE_UUID) -> dict:
return {
'iss': 'langbot-space',
'aud': 'langbot-cloud-runtime',
'sub': f'langbot-instance:{INSTANCE_UUID}',
'jti': jti or str(uuid.uuid4()),
'iat': now,
'nbf': now - 5,
'exp': now + 90,
'instance_uuid': INSTANCE_UUID,
'kind': 'workspace.launch',
'payload': {
'account_uuid': ACCOUNT_UUID,
'workspace_uuid': workspace_uuid,
},
}
def _service(private_key: Ed25519PrivateKey, *, now: int) -> SpaceLaunchService:
public_key = private_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
app = SimpleNamespace(
deployment=SimpleNamespace(multi_workspace_enabled=True, verification_key_id=KEY_ID),
workspace_service=SimpleNamespace(instance_uuid=INSTANCE_UUID),
instance_config=SimpleNamespace(
data={
'space': {
'launch': {
'control_plane_public_key': _base64url(public_key),
}
}
}
),
)
return SpaceLaunchService(app, wall_time=lambda: now)
async def test_consumes_valid_workspace_launch_assertion_once():
private_key = Ed25519PrivateKey.generate()
now = int(time.time())
service = _service(private_key, now=now)
token = _sign(private_key, _claims(now=now))
launch = await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
assert launch == {'account_uuid': ACCOUNT_UUID, 'workspace_uuid': WORKSPACE_UUID}
with pytest.raises(SpaceLaunchError, match='already been consumed'):
await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
async def test_rejects_expired_wrong_workspace_and_wrong_instance_assertions():
private_key = Ed25519PrivateKey.generate()
now = int(time.time())
service = _service(private_key, now=now)
expired = _claims(now=now)
expired['exp'] = now - 60
with pytest.raises(SpaceLaunchError, match='expired'):
await service.consume_assertion(_sign(private_key, expired), expected_workspace_uuid=WORKSPACE_UUID)
wrong_workspace = _sign(private_key, _claims(now=now, workspace_uuid='33333333-3333-4333-8333-333333333333'))
with pytest.raises(SpaceLaunchError, match='another Workspace'):
await service.consume_assertion(wrong_workspace, expected_workspace_uuid=WORKSPACE_UUID)
wrong_instance = _claims(now=now)
wrong_instance['instance_uuid'] = 'other-instance'
with pytest.raises(SpaceLaunchError, match='instance UUID'):
await service.consume_assertion(_sign(private_key, wrong_instance), expected_workspace_uuid=WORKSPACE_UUID)
async def test_rejects_invalid_signature_and_non_cloud_mode():
private_key = Ed25519PrivateKey.generate()
now = int(time.time())
token = _sign(private_key, _claims(now=now))
service = _service(Ed25519PrivateKey.generate(), now=now)
with pytest.raises(SpaceLaunchError, match='signature'):
await service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
oss_service = _service(private_key, now=now)
oss_service.ap.deployment.multi_workspace_enabled = False
with pytest.raises(SpaceLaunchError, match='verified Cloud mode'):
await oss_service.consume_assertion(token, expected_workspace_uuid=WORKSPACE_UUID)
@@ -0,0 +1,88 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from langbot.pkg.workspace.invitation_delivery import (
InvitationDeliveryResult,
InvitationDeliveryService,
)
pytestmark = pytest.mark.asyncio
def _app(config: dict) -> SimpleNamespace:
return SimpleNamespace(
instance_config=SimpleNamespace(data=config),
logger=SimpleNamespace(warning=lambda *args, **kwargs: None),
)
async def test_link_only_delivery_builds_public_web_link_without_secret_capability():
service = InvitationDeliveryService(
_app(
{
'api': {
'webui_url': 'https://public.langbot.example/',
'webhook_prefix': 'http://internal:5300',
}
}
)
)
assert service.build_invitation_link('lbi_secret') == (
'https://public.langbot.example/invitations/accept#token=lbi_secret'
)
assert service.capability() == {'enabled': False, 'provider': None}
result = await service.deliver_invitation(
recipient_email='member@example.com',
workspace_name='Workspace',
invitation_link='https://public.langbot.example/invitations/accept#token=lbi_secret',
)
assert result == InvitationDeliveryResult(status='link_only', provider=None)
async def test_configured_provider_failure_returns_failed_without_raising():
service = InvitationDeliveryService(
_app(
{
'workspace': {
'invitations': {
'email': {
'provider': 'resend',
'from': 'LangBot <noreply@example.com>',
'resend': {'api_key': 'resend-secret'},
}
}
}
}
)
)
service._send_resend = AsyncMock(return_value=False)
assert service.capability() == {'enabled': True, 'provider': 'resend'}
result = await service.deliver_invitation(
recipient_email='member@example.com',
workspace_name='Workspace',
invitation_link='https://public.langbot.example/invitations/accept#token=lbi_secret',
)
assert result == InvitationDeliveryResult(status='failed', provider='resend')
service._send_resend.assert_awaited_once()
async def test_environment_mapping_enables_provider_without_leaking_secret(monkeypatch):
monkeypatch.setenv('WORKSPACE__INVITATIONS__PUBLIC_WEB_URL', 'https://env.langbot.example/')
monkeypatch.setenv('WORKSPACE__INVITATIONS__EMAIL__PROVIDER', 'smtp')
monkeypatch.setenv('WORKSPACE__INVITATIONS__EMAIL__FROM', 'LangBot <noreply@example.com>')
monkeypatch.setenv('WORKSPACE__INVITATIONS__EMAIL__SMTP__HOST', 'smtp.example.com')
monkeypatch.setenv('WORKSPACE__INVITATIONS__EMAIL__SMTP__PASSWORD', 'smtp-secret')
service = InvitationDeliveryService(_app({'api': {'webui_url': 'https://config.example'}}))
assert service.build_invitation_link('lbi_secret') == (
'https://env.langbot.example/invitations/accept#token=lbi_secret'
)
assert service.capability() == {'enabled': True, 'provider': 'smtp'}
@@ -240,7 +240,7 @@ async def test_cloud_member_listing_opens_workspace_uow_when_request_scope_has_c
]
async def test_cloud_directory_requires_explicit_selector_and_rejects_local_mutation(
async def test_cloud_directory_requires_explicit_selector_and_allows_core_membership_management(
collaboration_context,
):
_service, workspace_service, session_factory, owner, _workspace, _membership = collaboration_context
@@ -292,13 +292,34 @@ async def test_cloud_directory_requires_explicit_selector_and_rejects_local_muta
assert access.workspace.uuid == cloud_workspace_uuid
assert access.execution.placement_generation == 8
with pytest.raises(MembershipPermissionError, match='control plane'):
await cloud_service.create_invitation(
cloud_workspace_uuid,
cloud_membership,
'member@example.com',
'viewer',
)
created = await cloud_service.create_invitation(
cloud_workspace_uuid,
cloud_membership,
'member@example.com',
'viewer',
)
assert created.invitation.workspace_uuid == cloud_workspace_uuid
assert created.token.startswith('lbi_')
member = await _add_account(session_factory, 'member@example.com')
membership = await cloud_service.accept_invitation(created.token, member.uuid)
assert membership.workspace_uuid == cloud_workspace_uuid
assert membership.role == 'viewer'
updated = await cloud_service.update_member_role(
cloud_workspace_uuid,
member.uuid,
'developer',
cloud_membership,
)
assert updated.role == 'developer'
removed = await cloud_service.remove_member(
cloud_workspace_uuid,
member.uuid,
cloud_membership,
)
assert removed.status == 'removed'
async def test_invitation_lock_registry_does_not_retain_sequential_tokens(collaboration_context):