Merge remote-tracking branch 'origin/dev/4.11.x' into dev/4.11.x

# Conflicts:
#	uv.lock
#	web/src/i18n/locales/en-US.ts
#	web/src/i18n/locales/ja-JP.ts
#	web/src/i18n/locales/zh-Hans.ts
#	web/src/i18n/locales/zh-Hant.ts
This commit is contained in:
fdc310
2026-09-19 11:00:45 +08:00
357 changed files with 43727 additions and 6682 deletions
+15
View File
@@ -148,6 +148,21 @@ class TestEmbedWidgetEndpoint:
assert 'javascript' in response.content_type
fake_embed_app.platform_mgr.resolve_public_bot.assert_any_await('a1b2c3d4-5678-90ab-cdef-123456789abc')
@pytest.mark.asyncio
@pytest.mark.parametrize(
('query', 'expected_base'),
[('', 'https://public.example/bot'), ('?preview=wizard', 'http://localhost')],
)
async def test_widget_preview_uses_current_backend(
self, quart_test_client, fake_embed_app, monkeypatch, query, expected_base
):
monkeypatch.setitem(fake_embed_app.instance_config.data['api'], 'webhook_prefix', 'https://public.example/bot')
response = await quart_test_client.get('/api/v1/embed/a1b2c3d4-5678-90ab-cdef-123456789abc/widget.js' + query)
assert response.status_code == 200
body = await response.get_data(as_text=True)
assert f'baseUrl: "{expected_base}"' in body
assert f'logoUrl: "{expected_base}"' in body
def test_widget_template_cache_reloads_after_file_change(self, monkeypatch, tmp_path):
"""Development edits to widget.js take effect without restarting the backend."""
import langbot.pkg.api.http.controller.groups.pipelines.embed as embed
+35 -6
View File
@@ -9,7 +9,7 @@ Run: uv run pytest tests/integration/api/test_monitoring.py -q
from __future__ import annotations
import pytest
from unittest.mock import MagicMock, AsyncMock, Mock
from unittest.mock import MagicMock, AsyncMock, Mock, patch
from types import SimpleNamespace
from tests.factories import FakeApp
@@ -242,6 +242,22 @@ class TestMonitoringSessionsEndpoint:
assert response.status_code == 200
@pytest.mark.asyncio
async def test_get_sessions_forwards_user_search_and_page_window(self, quart_test_client, fake_monitoring_app):
fake_monitoring_app.monitoring_service.get_sessions.reset_mock()
response = await quart_test_client.get(
'/api/v1/monitoring/sessions?botId=bot-1&userQuery=alice&limit=20&offset=40',
headers={'Authorization': 'Bearer test_token'},
)
assert response.status_code == 200
kwargs = fake_monitoring_app.monitoring_service.get_sessions.await_args.kwargs
assert kwargs['bot_ids'] == ['bot-1']
assert kwargs['user_query'] == 'alice'
assert kwargs['limit'] == 20
assert kwargs['offset'] == 40
@pytest.mark.usefixtures('mock_circular_import_chain')
class TestMonitoringErrorsEndpoint:
@@ -264,13 +280,20 @@ class TestMonitoringAllDataEndpoint:
@pytest.mark.asyncio
async def test_get_all_data_success(self, quart_test_client):
"""GET /api/v1/monitoring/data returns all data."""
response = await quart_test_client.get(
'/api/v1/monitoring/data', headers={'Authorization': 'Bearer test_token'}
)
traffic = {'series': [], 'truncated': False}
with patch(
'langbot.pkg.api.http.controller.groups.monitoring.get_traffic_series',
new=AsyncMock(return_value=traffic),
) as get_traffic:
response = await quart_test_client.get(
'/api/v1/monitoring/data', headers={'Authorization': 'Bearer test_token'}
)
get_traffic.assert_awaited_once()
assert response.status_code == 200
data = await response.get_json()
assert 'overview' in data['data']
assert data['data']['traffic'] == traffic
@pytest.mark.usefixtures('mock_circular_import_chain')
@@ -278,13 +301,19 @@ class TestMonitoringDetailsEndpoints:
"""Tests for detail endpoints."""
@pytest.mark.asyncio
async def test_get_session_analysis(self, quart_test_client):
async def test_get_session_analysis(self, quart_test_client, fake_monitoring_app):
"""GET /api/v1/monitoring/sessions/{id}/analysis."""
response = await quart_test_client.get(
'/api/v1/monitoring/sessions/sess-1/analysis', headers={'Authorization': 'Bearer test_token'}
'/api/v1/monitoring/sessions/sess-1/analysis'
'?startTime=2026-08-31T16%3A00%3A00.000Z'
'&endTime=2026-09-01T15%3A59%3A59.999Z',
headers={'Authorization': 'Bearer test_token'},
)
assert response.status_code == 200
kwargs = fake_monitoring_app.monitoring_service.get_session_analysis.await_args.kwargs
assert kwargs['start_time'].isoformat() == '2026-08-31T16:00:00'
assert kwargs['end_time'].isoformat() == '2026-09-01T15:59:59.999000'
@pytest.mark.asyncio
async def test_get_message_details(self, quart_test_client):
@@ -0,0 +1,85 @@
from __future__ import annotations
import logging
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from quart import Quart
from langbot.pkg.api.http.controller.groups import user as user_module
from langbot.pkg.api.http.controller.groups.user import UserRouterGroup
from langbot.pkg.api.http.service.user import UserService
from langbot.pkg.core.stages.genkeys import GenKeysStage
from langbot.pkg.persistence.mgr import PersistenceManager
from langbot.pkg.utils import constants
from langbot.pkg.workspace.collaboration import WorkspaceCollaborationService
from langbot.pkg.workspace.service import WorkspaceService
pytestmark = [pytest.mark.integration, pytest.mark.asyncio]
async def test_generated_recovery_code_resets_real_sqlite_account(tmp_path, monkeypatch):
"""Exercise generation, reset, and old/new password login without mocked user services."""
monkeypatch.setattr(constants, 'instance_id', 'recovery-journey')
monkeypatch.setattr(user_module, '_reset_password_state', {'window_started_at': 0.0, 'attempts': 0})
monkeypatch.setattr(user_module, 'asyncio', SimpleNamespace(sleep=AsyncMock()))
application = SimpleNamespace(
logger=logging.getLogger('recovery-password-journey'),
instance_config=SimpleNamespace(
data={
'database': {'use': 'sqlite', 'sqlite': {'path': str(tmp_path / 'recovery.db')}},
'system': {
'jwt': {'secret': 'recovery-journey-test-secret-only', 'expire': 3600},
'recovery_key': '',
},
},
dump_config=AsyncMock(),
),
)
await GenKeysStage().run(application)
key = application.instance_config.data['system']['recovery_key']
assert len(key) == 8
assert set(key) <= set('23456789ABCDEFGHJKLMNPQRSTUVWXYZ')
persistence = PersistenceManager(application)
application.persistence_mgr = persistence
try:
await persistence.initialize()
application.workspace_service = WorkspaceService(application, instance_uuid='recovery-journey')
application.workspace_collaboration_service = WorkspaceCollaborationService(
application, application.workspace_service
)
application.user_service = UserService(application)
quart_app = Quart(__name__)
await UserRouterGroup(application, quart_app).initialize()
client = quart_app.test_client()
initial = await client.post(
'/api/v1/user/init', json={'user': 'owner@example.com', 'password': 'OriginalPass1!'}
)
assert initial.status_code == 200
assert (await initial.get_json())['code'] == 0
payload = {'user': 'owner@example.com', 'recovery_key': 'WRONG', 'new_password': 'RecoveredPass1!'}
wrong = await client.post('/api/v1/user/reset-password', json=payload)
assert wrong.status_code == 403
unchanged = await client.post(
'/api/v1/user/auth', json={'user': 'owner@example.com', 'password': 'OriginalPass1!'}
)
assert (await unchanged.get_json())['code'] == 0
reset = await client.post('/api/v1/user/reset-password', json={**payload, 'recovery_key': key})
assert reset.status_code == 200
assert (await reset.get_json())['code'] == 0
old_login = await client.post(
'/api/v1/user/auth', json={'user': 'owner@example.com', 'password': 'OriginalPass1!'}
)
assert (await old_login.get_json())['code'] != 0
new_login = await client.post(
'/api/v1/user/auth', json={'user': 'owner@example.com', 'password': 'RecoveredPass1!'}
)
new_data = await new_login.get_json()
assert new_data['code'] == 0
assert new_data['data']['token']
finally:
await persistence.get_db_engine().dispose()
+6
View File
@@ -310,6 +310,8 @@ class TestUserInitEndpoint:
'invitation_registration_enabled': True,
'password_login_enabled': True,
'space_login_enabled': False,
'passkey_login_enabled': True,
'passkey_supported': True,
}
fake_api_app.user_service.get_login_capabilities.assert_awaited_once_with()
fake_api_app.user_service.get_first_user.assert_not_awaited()
@@ -334,6 +336,8 @@ class TestUserInitEndpoint:
'invitation_registration_enabled': False,
'password_login_enabled': False,
'space_login_enabled': True,
'passkey_login_enabled': True,
'passkey_supported': True,
}
@pytest.mark.asyncio
@@ -355,6 +359,8 @@ class TestUserInitEndpoint:
'invitation_registration_enabled': True,
'password_login_enabled': False,
'space_login_enabled': True,
'passkey_login_enabled': True,
'passkey_supported': True,
}
@pytest.mark.asyncio
@@ -0,0 +1,190 @@
"""
Integration smoke tests for Passkey API endpoints.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, Mock
import pytest
from tests.factories import FakeApp
from tests.utils.import_isolation import isolated_sys_modules, MockLifecycleControlScope
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures('mock_circular_import_chain')]
@pytest.fixture(scope='module')
def mock_circular_import_chain():
class FakeMinimalApplication:
pass
mock_app = Mock()
mock_app.Application = FakeMinimalApplication
mock_entities = Mock()
mock_entities.LifecycleControlScope = MockLifecycleControlScope
clear = [
'langbot.pkg.api.http.controller.group',
'langbot.pkg.api.http.controller.groups',
'langbot.pkg.api.http.controller.groups.system',
'langbot.pkg.api.http.controller.groups.user',
'langbot.pkg.api.http.controller.main',
]
with isolated_sys_modules(
mocks={
'langbot.pkg.core.app': mock_app,
'langbot.pkg.core.entities': mock_entities,
},
clear=clear,
):
import langbot.pkg.api.http.controller.groups.user as _user_group # noqa: E402, F401
yield
@pytest.fixture
def fake_api_app():
app = FakeApp()
app.instance_config.data.update(
{
'api': {'port': 5300},
'system': {'allow_modify_login_info': True},
}
)
app.user_service = Mock()
app.user_service.verify_jwt_token = AsyncMock(side_effect=ValueError('Invalid token'))
app.user_service.get_user_by_email = AsyncMock(return_value=Mock())
return app
@pytest.fixture
async def quart_test_client(fake_api_app, http_controller_cls):
controller = http_controller_cls(fake_api_app)
await controller.initialize()
client = controller.quart_app.test_client()
yield client
class TestPasskeyPublicEndpoints:
@pytest.mark.asyncio
async def test_auth_options_endpoint(self, quart_test_client, fake_api_app):
fake_api_app.user_service.generate_passkey_authentication_options = AsyncMock(
return_value=({'challenge': 'test_chal', 'rpId': 'localhost'}, 'token_123')
)
response = await quart_test_client.post(
'/api/v1/user/passkey/auth/options',
json={'origin': 'http://localhost:3000'},
)
assert response.status_code == 200
data = await response.get_json()
assert data['code'] == 0
assert data['data']['challenge_token'] == 'token_123'
assert data['data']['options']['rpId'] == 'localhost'
@pytest.mark.asyncio
async def test_auth_verify_missing_payload(self, quart_test_client, fake_api_app):
response = await quart_test_client.post(
'/api/v1/user/passkey/auth/verify',
json={},
)
assert response.status_code == 200
data = await response.get_json()
assert data['code'] != 0
assert 'Missing challenge_token or credential' in data['msg']
@pytest.mark.asyncio
async def test_auth_verify_success(self, quart_test_client, fake_api_app):
fake_api_app.user_service.verify_passkey_authentication = AsyncMock(
return_value=('jwt_token_abc', Mock(user='user@example.com'))
)
response = await quart_test_client.post(
'/api/v1/user/passkey/auth/verify',
json={'challenge_token': 'token_123', 'credential': {'id': 'cred_id'}},
)
assert response.status_code == 200
data = await response.get_json()
assert data['code'] == 0
assert data['data']['token'] == 'jwt_token_abc'
assert data['data']['user'] == 'user@example.com'
class TestPasskeyProtectedEndpoints:
@pytest.mark.asyncio
async def test_register_options_requires_auth(self, quart_test_client):
response = await quart_test_client.post('/api/v1/user/passkey/register/options', json={})
assert response.status_code == 401
@pytest.mark.asyncio
async def test_passkeys_list_requires_auth(self, quart_test_client):
response = await quart_test_client.get('/api/v1/user/passkeys')
assert response.status_code == 401
class TestPasskeyReverseProxyScenarios:
@pytest.mark.asyncio
async def test_auth_options_respects_custom_origin_body_behind_proxy(self, quart_test_client, fake_api_app):
fake_api_app.user_service.generate_passkey_authentication_options = AsyncMock(
return_value=({'challenge': 'test_chal', 'rpId': 'proxy.company.com'}, 'token_proxy')
)
response = await quart_test_client.post(
'/api/v1/user/passkey/auth/options',
json={'origin': 'https://proxy.company.com:8443'},
headers={'Host': '127.0.0.1:5300'},
)
assert response.status_code == 200
data = await response.get_json()
assert data['code'] == 0
fake_api_app.user_service.generate_passkey_authentication_options.assert_awaited_once_with(
rp_id='proxy.company.com',
origin='https://proxy.company.com:8443',
email=None,
)
@pytest.mark.asyncio
async def test_auth_options_falls_back_to_origin_header(self, quart_test_client, fake_api_app):
fake_api_app.user_service.generate_passkey_authentication_options = AsyncMock(
return_value=({'challenge': 'test_chal', 'rpId': 'bot.example.com'}, 'token_header')
)
response = await quart_test_client.post(
'/api/v1/user/passkey/auth/options',
json={},
headers={'Origin': 'https://bot.example.com'},
)
assert response.status_code == 200
fake_api_app.user_service.generate_passkey_authentication_options.assert_awaited_once_with(
rp_id='bot.example.com',
origin='https://bot.example.com',
email=None,
)
@pytest.mark.asyncio
async def test_auth_options_falls_back_to_referer_header(self, quart_test_client, fake_api_app):
fake_api_app.user_service.generate_passkey_authentication_options = AsyncMock(
return_value=({'challenge': 'test_chal', 'rpId': 'bot.example.com'}, 'token_referer')
)
response = await quart_test_client.post(
'/api/v1/user/passkey/auth/options',
json={},
headers={'Referer': 'https://bot.example.com:9000/login'},
)
assert response.status_code == 200
fake_api_app.user_service.generate_passkey_authentication_options.assert_awaited_once_with(
rp_id='bot.example.com',
origin='https://bot.example.com:9000',
email=None,
)
+198 -8
View File
@@ -27,7 +27,8 @@ async def space_oauth_api():
execution=SimpleNamespace(instance_uuid='instance-a', placement_generation=1),
)
application = Mock()
application.deployment = SimpleNamespace(multi_workspace_enabled=False)
application.deployment = SimpleNamespace(multi_workspace_enabled=False, mode='oss')
application.directory_projection_service = None
application.persistence_mgr = None
application.user_service.get_authenticated_account = AsyncMock(return_value=account)
application.user_service.issue_space_oauth_state = AsyncMock(
@@ -125,6 +126,26 @@ async def test_cloud_launch_state_is_server_issued_and_workspace_bound(space_oau
)
@pytest.mark.asyncio
async def test_cloud_login_entry_uses_normal_stateful_oauth(space_oauth_api):
application, client = space_oauth_api
application.deployment.mode = 'cloud'
response = await client.get(
'/api/v1/user/space/authorize-url',
query_string={
'redirect_uri': 'http://localhost/auth/space/callback',
'cloud_entry': '1',
},
headers={'Origin': 'http://localhost'},
)
assert response.status_code == 200
authorize_url = (await response.get_json())['data']['authorize_url']
assert authorize_url.startswith('https://space.example/authorize?state=')
application.user_service.issue_space_oauth_state.assert_awaited_once_with('login')
@pytest.mark.asyncio
async def test_public_login_rejects_caller_supplied_state(space_oauth_api):
application, client = space_oauth_api
@@ -249,10 +270,14 @@ async def test_server_side_webhook_origin_supports_bundled_ui(space_oauth_api):
async def test_login_callback_requires_and_consumes_server_state(space_oauth_api):
application, client = space_oauth_api
missing = await client.post('/api/v1/user/space/callback', json={'code': 'oauth-code'})
missing = await client.post('/api/v1/user/space/callback', json={'code': 'v4_oauth-code'})
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'oauth-code', 'state': 'opaque-login-state'},
json={
'code': 'v4_oauth-code',
'state': 'opaque-login-state',
'redirect_uri': 'https://oss.example/auth/space/callback',
},
)
assert (await missing.get_json())['code'] == 1
@@ -260,12 +285,146 @@ async def test_login_callback_requires_and_consumes_server_state(space_oauth_api
assert (await response.get_json())['data']['token'] == 'space-login-token'
application.user_service.consume_space_oauth_state_details.assert_awaited_once_with('opaque-login-state', 'login')
application.space_service.exchange_oauth_code.assert_awaited_once_with(
'oauth-code',
'v4_oauth-code',
[WORKSPACE_UUID],
{WORKSPACE_UUID: int(WORKSPACE_CREATED_AT.timestamp())},
redirect_uri='https://oss.example/auth/space/callback',
)
@pytest.mark.asyncio
async def test_login_callback_rejects_downgraded_legacy_code(space_oauth_api):
application, client = space_oauth_api
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v2_legacy-code', 'state': 'opaque-login-state'},
)
payload = await response.get_json()
assert response.status_code == 200
assert payload['code'] == 1
assert 'code contract' in payload['msg']
application.space_service.exchange_oauth_code.assert_not_awaited()
@pytest.mark.asyncio
async def test_cloud_login_callback_reconciles_authorized_workspace_before_local_authentication(space_oauth_api):
application, client = space_oauth_api
application.deployment.mode = 'cloud'
calls: list[str] = []
application.directory_projection_service = SimpleNamespace(
reconcile_workspaces=AsyncMock(side_effect=lambda _workspace_uuids: calls.append('reconcile'))
)
application.space_service.exchange_oauth_code.return_value = {
'access_token': 'space-access-token',
'refresh_token': 'space-refresh-token',
'expires_in': 3600,
'cloud_workspace_uuid': WORKSPACE_UUID,
}
authenticated_account = application.user_service.authenticate_space_user.return_value[1]
async def authenticate(*_args):
calls.append('authenticate')
return 'space-login-token', authenticated_account
application.user_service.authenticate_space_user.side_effect = authenticate
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
assert response.status_code == 200
assert (await response.get_json())['data']['workspace_uuid'] == WORKSPACE_UUID
assert calls == ['reconcile', 'authenticate']
application.directory_projection_service.reconcile_workspaces.assert_awaited_once_with((WORKSPACE_UUID,))
@pytest.mark.asyncio
async def test_cloud_login_callback_fails_closed_without_workspace_binding(space_oauth_api):
application, client = space_oauth_api
application.deployment.mode = 'cloud'
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
payload = await response.get_json()
assert response.status_code == 200
assert payload['code'] == 1
assert 'Cloud Workspace binding' in payload['msg']
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
application.user_service.authenticate_space_user.assert_not_awaited()
@pytest.mark.asyncio
async def test_cloud_login_callback_requires_code_binding_for_launch_state(space_oauth_api):
application, client = space_oauth_api
application.deployment.mode = 'cloud'
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
application.user_service.consume_space_oauth_state_details.return_value = SimpleNamespace(
launch_workspace_uuid=WORKSPACE_UUID
)
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
payload = await response.get_json()
assert response.status_code == 200
assert payload['code'] == 1
assert 'Workspace binding' in payload['msg']
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
application.user_service.authenticate_space_user.assert_not_awaited()
@pytest.mark.asyncio
async def test_cloud_login_callback_rejects_conflicting_state_and_code_workspace_bindings(space_oauth_api):
application, client = space_oauth_api
application.deployment.mode = 'cloud'
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
application.user_service.consume_space_oauth_state_details.return_value = SimpleNamespace(
launch_workspace_uuid=WORKSPACE_UUID
)
application.space_service.exchange_oauth_code.return_value = {
'access_token': 'space-access-token',
'refresh_token': 'space-refresh-token',
'expires_in': 3600,
'cloud_workspace_uuid': 'workspace-from-another-flow',
}
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
payload = await response.get_json()
assert response.status_code == 200
assert payload['code'] == 1
assert 'Workspace binding' in payload['msg']
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
application.user_service.authenticate_space_user.assert_not_awaited()
@pytest.mark.asyncio
async def test_oss_login_callback_does_not_request_cloud_reconciliation(space_oauth_api):
application, client = space_oauth_api
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
assert response.status_code == 200
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
@pytest.mark.asyncio
async def test_login_callback_launch_state_selects_asserted_workspace(space_oauth_api):
application, client = space_oauth_api
@@ -276,7 +435,7 @@ async def test_login_callback_launch_state_selects_asserted_workspace(space_oaut
response = await client.post(
'/api/v1/user/space/callback',
json={'code': 'oauth-code', 'state': 'opaque-login-state'},
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
)
assert response.status_code == 200
@@ -375,18 +534,22 @@ async def test_bind_callback_uses_opaque_state_and_never_treats_it_as_jwt(space_
rejected = await client.post(
'/api/v1/user/bind-space',
json={'code': 'attacker-code', 'state': 'jwt.must-not-be-used'},
json={'code': 'v4_attacker-code', 'state': 'jwt.must-not-be-used'},
)
response = await client.post(
'/api/v1/user/bind-space',
json={'code': 'oauth-code', 'state': 'opaque-bind-state'},
json={'code': 'v4_oauth-code', 'state': 'opaque-bind-state'},
)
assert rejected.status_code == 401
assert response.status_code == 200
assert (await response.get_json())['data']['token'] == 'rotated-account-token'
application.user_service.verify_jwt_token.assert_not_awaited()
application.user_service.bind_space_account.assert_awaited_once_with('owner@example.com', 'oauth-code')
application.user_service.bind_space_account.assert_awaited_once_with(
'owner@example.com',
'v4_oauth-code',
redirect_uri='http://localhost/auth/space/callback?mode=bind',
)
@pytest.mark.asyncio
@@ -394,6 +557,7 @@ async def test_direct_launch_assertion_does_not_consume_normal_oauth_state(space
application, client = space_oauth_api
application.user_service.consume_space_oauth_state.reset_mock()
application.space_service.exchange_oauth_code.reset_mock()
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
response = await client.post(
'/api/v1/user/space/callback',
@@ -414,3 +578,29 @@ async def test_direct_launch_assertion_does_not_consume_normal_oauth_state(space
)
application.user_service.consume_space_oauth_state.assert_not_awaited()
application.space_service.exchange_oauth_code.assert_not_awaited()
@pytest.mark.asyncio
async def test_direct_launch_reconciles_exact_workspace_before_resolving_access(space_oauth_api):
application, client = space_oauth_api
projected_account = SimpleNamespace(
uuid='account-a',
user='owner@example.com',
account_type='space',
status='active',
)
application.user_service.get_user_by_uuid = AsyncMock(return_value=projected_account)
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
response = await client.post(
'/api/v1/user/space/callback',
json={
'workspace_uuid': WORKSPACE_UUID,
'launch_assertion': 'signed-launch-token',
},
)
assert response.status_code == 200
assert (await response.get_json())['data']['workspace_uuid'] == WORKSPACE_UUID
application.directory_projection_service.reconcile_workspaces.assert_awaited_once_with((WORKSPACE_UUID,))
application.user_service.get_user_by_uuid.assert_awaited_once_with('account-a')
+260
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import datetime
import json
import logging
from types import SimpleNamespace
@@ -22,6 +23,7 @@ from langbot.pkg.api.http.service.apikey import ApiKeyService
from langbot.pkg.api.http.service.user import ControlPlaneDirectoryRequiredError, UserService
from langbot.pkg.entity.persistence.base import Base
from langbot.pkg.entity.persistence.metadata import WorkspaceMetadata
from langbot.pkg.entity.persistence import apikey
from langbot.pkg.entity.persistence.user import User
from langbot.pkg.entity.persistence.workspace import (
Workspace,
@@ -440,6 +442,264 @@ async def test_api_key_secret_is_one_time_and_viewer_cannot_manage_keys(workspac
assert (await forbidden.get_json())['code'] == 'permission_denied'
async def test_api_key_context_returns_bound_identity_without_workspace_permission(workspace_api):
application, client, _, owner_token = workspace_api
current_response = await client.get('/api/v1/workspaces/current', headers=_auth(owner_token))
workspace_uuid = (await current_response.get_json())['data']['workspace']['uuid']
create_response = await client.post(
'/api/v1/apikeys',
headers=_auth(owner_token, workspace_uuid),
json={'name': 'Context probe', 'scopes': []},
)
assert create_response.status_code == 200
created = (await create_response.get_json())['data']['key']
missing_auth = await client.get('/api/v1/system/context')
assert missing_auth.status_code == 401
invalid_auth = await client.get(
'/api/v1/system/context',
headers={'X-API-Key': 'lbk_invalid'},
)
assert invalid_auth.status_code == 401
invalid_capabilities = await client.get(
'/api/v1/system/capabilities',
headers={'X-API-Key': 'lbk_invalid'},
)
assert invalid_capabilities.status_code == 401
response = await client.get(
'/api/v1/system/context',
headers={
'X-API-Key': created['key'],
'X-Workspace-Id': 'caller-selected-workspace-must-be-ignored',
},
)
assert response.status_code == 200
assert (await response.get_json())['data'] == {
'instance_uuid': application.workspace_service.instance_uuid,
'workspace_uuid': workspace_uuid,
'api_key_id': created['uuid'],
'permissions': [],
}
capabilities_response = await client.get(
'/api/v1/system/capabilities',
headers={
'X-API-Key': created['key'],
'X-Workspace-Id': 'caller-selected-workspace-must-be-ignored',
},
)
assert capabilities_response.status_code == 200
capabilities = (await capabilities_response.get_json())['data']
assert capabilities['schema_version'] == 1
assert sorted(capabilities['operations']) == sorted(
[
'bot.list',
'bot.get',
'bot.create',
'bot.update',
'bot.delete',
'pipeline.list',
'pipeline.get',
'pipeline.create',
'pipeline.update',
'pipeline.delete',
'pipeline.copy',
'task.list',
'task.get',
'knowledge_base.list',
'knowledge_base.get',
'knowledge_base.create',
'knowledge_base.update',
'knowledge_base.delete',
'knowledge_base.file.list',
'knowledge_base.file.store',
'knowledge_base.file.delete',
'knowledge_base.retrieve',
'file.document.upload',
'plugin.install.github',
'plugin.install.marketplace',
'plugin.install.local',
'plugin.upgrade',
'plugin.get',
'plugin.list',
'plugin.config.get',
'plugin.config.update',
'plugin.logs',
'plugin.delete',
'provider.list',
'provider.get',
'provider.create',
'provider.update',
'provider.delete',
'provider.scan_models',
'model.llm.list',
'model.llm.get',
'model.llm.create',
'model.llm.update',
'model.llm.delete',
'model.llm.test',
'model.embedding.list',
'model.embedding.get',
'model.embedding.create',
'model.embedding.update',
'model.embedding.delete',
'model.embedding.test',
'model.rerank.list',
'model.rerank.get',
'model.rerank.create',
'model.rerank.update',
'model.rerank.delete',
'model.rerank.test',
'skill.list',
'skill.get',
'skill.create',
'skill.update',
'skill.delete',
'skill.files.list',
'skill.files.read',
'skill.files.write',
'skill.preview',
'skill.install.github',
'skill.install.upload',
'mcp_server.list',
'mcp_server.get',
'mcp_server.create',
'mcp_server.update',
'mcp_server.delete',
'mcp_server.resources',
'mcp_server.resource_templates',
'mcp_server.resource_read',
'mcp_server.logs',
'mcp_server.test',
]
)
assert all(item == {'supported': True} for item in capabilities['operations'].values())
assert created['key'] not in await capabilities_response.get_data(as_text=True)
bearer_response = await client.get(
'/api/v1/system/context',
headers={'Authorization': f'Bearer {created["key"]}'},
)
assert bearer_response.status_code == 200
assert (await bearer_response.get_json())['data']['api_key_id'] == created['uuid']
jwt_response = await client.get(
'/api/v1/system/context',
headers={'Authorization': f'Bearer {owner_token}'},
)
assert jwt_response.status_code == 401
await application.persistence_mgr.execute_async(
sqlalchemy.update(apikey.ApiKey)
.where(apikey.ApiKey.uuid == created['uuid'])
.values(expires_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - datetime.timedelta(seconds=1))
)
expired_capabilities = await client.get(
'/api/v1/system/capabilities',
headers={'X-API-Key': created['key']},
)
assert expired_capabilities.status_code == 401
revoke_response = await client.delete(
f'/api/v1/apikeys/{created["id"]}',
headers=_auth(owner_token, workspace_uuid),
)
assert revoke_response.status_code == 200
revoked_response = await client.get(
'/api/v1/system/context',
headers={'X-API-Key': created['key']},
)
assert revoked_response.status_code == 401
revoked_capabilities = await client.get(
'/api/v1/system/capabilities',
headers={'X-API-Key': created['key']},
)
assert revoked_capabilities.status_code == 401
async def test_api_key_can_query_tasks_with_public_contract_and_resource_permission(workspace_api):
application, client, _, owner_token = workspace_api
task_query = {}
task_lookup = {}
fake_task = SimpleNamespace(
to_public_dict=lambda: {'id': 7, 'status': 'running', 'error': None, 'result': None},
to_dict=lambda: {'id': 7, 'runtime': {'state': 'PENDING'}},
)
def get_tasks_dict(*args, **kwargs):
task_query.update(kwargs)
if kwargs.get('public'):
return {'tasks': []}
return {'tasks': [], 'id_index': 1}
def get_task_by_id(*args, **kwargs):
task_lookup.update(kwargs)
return fake_task if args and args[0] == 7 else None
application.task_mgr = SimpleNamespace(
get_tasks_dict=get_tasks_dict,
get_task_by_id=get_task_by_id,
)
current_response = await client.get('/api/v1/workspaces/current', headers=_auth(owner_token))
workspace_uuid = (await current_response.get_json())['data']['workspace']['uuid']
create_response = await client.post(
'/api/v1/apikeys',
headers=_auth(owner_token, workspace_uuid),
json={'name': 'Task reader', 'scopes': ['resource.view']},
)
assert create_response.status_code == 200
key = (await create_response.get_json())['data']['key']['key']
listing = await client.get('/api/v1/system/tasks', headers={'X-API-Key': key})
assert listing.status_code == 200
assert (await listing.get_json())['data'] == {'tasks': []}
assert task_query['instance_uuid'] == application.workspace_service.instance_uuid
assert task_query['workspace_uuid'] == workspace_uuid
assert task_query['placement_generation'] == 1
assert task_query['public'] is True
bearer_listing = await client.get('/api/v1/system/tasks', headers=_auth(owner_token, workspace_uuid))
assert bearer_listing.status_code == 200
assert (await bearer_listing.get_json())['data'] == {'tasks': [], 'id_index': 1}
public_task = await client.get('/api/v1/system/tasks/7', headers={'X-API-Key': key})
assert public_task.status_code == 200
assert (await public_task.get_json())['data'] == {
'id': 7,
'status': 'running',
'error': None,
'result': None,
}
assert task_lookup == {
'instance_uuid': application.workspace_service.instance_uuid,
'workspace_uuid': workspace_uuid,
'placement_generation': 1,
}
legacy_task = await client.get('/api/v1/system/tasks/7', headers=_auth(owner_token, workspace_uuid))
assert legacy_task.status_code == 200
assert (await legacy_task.get_json())['data'] == {'id': 7, 'runtime': {'state': 'PENDING'}}
missing = await client.get('/api/v1/system/tasks/not-an-id', headers={'X-API-Key': key})
assert missing.status_code == 404
no_permission_response = await client.post(
'/api/v1/apikeys',
headers=_auth(owner_token, workspace_uuid),
json={'name': 'Task denied', 'scopes': []},
)
assert no_permission_response.status_code == 200
no_permission_key = (await no_permission_response.get_json())['data']['key']['key']
denied = await client.get('/api/v1/system/tasks', headers={'X-API-Key': no_permission_key})
assert denied.status_code == 403
async def test_cloud_projection_is_selected_explicitly_and_collaboration_runs_in_core(
workspace_api,
):
@@ -0,0 +1,50 @@
# Historical schema fixtures
These JSON files contain **DDL compiled from historical ORM modules**, not the
current models with a historical revision stamped over them. `source_commit`,
`source_path`, and `revision` in each file record provenance.
- `baseline_schema.json`: the first Alembic baseline (database version 25),
commit `9cd3544d59600fcb88700d05e4b211f59ac00445`.
- `master_schema.json`: commit `9b7ba0d64708496ace30a82866f6dbc185f089dc`,
published head `0024_passkey_credentials`.
- `beta_schema.json`: commit `03854b5d33d8b66fec4c86a77714e0e7512a31bd`,
published head `0025_bot_plugin_processors`.
`test_migration_branch_convergence.py` loads the appropriate SQLite/PostgreSQL
DDL into an empty database, executes the real ancestors to the exact historical
revision, seeds representative rows, then upgrades to the unified head. There
is no version stamping in these regression tests. Historical ORM startup used
`create_all` before Alembic; executing ancestors also installs PostgreSQL RLS
and pgvector objects absent from the ORM snapshots.
The baseline PostgreSQL case follows the application's staged legacy startup:
upgrade to `0010_scope_resources`, create deferred tenant tables, then upgrade
to head. Bare Alembic against a completely empty PostgreSQL database is **not**
the product's fresh-install contract; the fresh case explicitly starts empty,
creates current ORM metadata, and executes all migrations. Existing-table
columns and populated baseline data still undergo real migrations.
The data probes preserve account/owner membership, bot routes, pipelines,
providers, messages, colliding bot sessions, beta Agent/Runner state and plugin
processor subscriptions, and master Codex/passkey rows. PostgreSQL checks RLS
flags/policies; SQLite checks foreign-key integrity. This is representative
migration coverage, not a production database clone or exhaustive data fuzzing.
PostgreSQL tests require `TEST_POSTGRES_URL` to an expendable test service. Each
case creates and drops only its own UUID-named schema. Prefer a disposable
`pgvector/pgvector:pg16` container with loopback-only port binding, CPU/memory
limits, and tmpfs storage. Do not point the suite at production.
The only downgrade exercised with populated branch data removes the no-op
merge and leaves **both** parent heads. Older feature downgrades can destroy
credentials, agents, or colliding bot sessions and are not claimed safe.
## Regeneration
Run `generate_schema_fixtures.py` with the repository's locked Python environment
and `PYTHONPATH=src` from the repository root. It reads historical objects with
`git show` into a temporary package and compiles each historical metadata set
using SQLAlchemy's SQLite/PostgreSQL dialects; it never modifies migrations or
connects to a database. Keep these fixed historical snapshots when adding new
migrations; do not regenerate from current models.
@@ -0,0 +1,113 @@
{
"source_commit": "9cd3544d59600fcb88700d05e4b211f59ac00445",
"source_path": "src/langbot/pkg/entity/persistence/",
"revision": "0001_baseline",
"dialects": {
"sqlite": [
"CREATE TABLE api_keys (\n\tid INTEGER NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(512), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tUNIQUE (\"key\")\n)",
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tuse_pipeline_name VARCHAR(255), \n\tuse_pipeline_uuid VARCHAR(255), \n\tpipeline_routing_rules JSON DEFAULT '[]' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE binary_storages (\n\tunique_key VARCHAR(255) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BLOB NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (unique_key)\n)",
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE metadata (\n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (\"key\")\n)",
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
"CREATE TABLE monitoring_sessions (\n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time DATETIME NOT NULL, \n\tlast_activity DATETIME NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (session_id)\n)",
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
"CREATE UNIQUE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tstarted_at DATETIME NOT NULL, \n\tfinished_at DATETIME NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE plugin_settings (\n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (plugin_author, plugin_name)\n)",
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME, \n\tupdated_at DATETIME, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at DATETIME, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE users (\n\tid INTEGER NOT NULL, \n\tuser VARCHAR(255) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at DATETIME, \n\tspace_api_key VARCHAR(255), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id)\n)",
"CREATE TABLE webhooks (\n\tid INTEGER NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id)\n)"
],
"postgresql": [
"CREATE TABLE api_keys (\n\tid SERIAL NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(512), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tUNIQUE (key)\n)",
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tuse_pipeline_name VARCHAR(255), \n\tuse_pipeline_uuid VARCHAR(255), \n\tpipeline_routing_rules JSON DEFAULT '[]' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE binary_storages (\n\tunique_key VARCHAR(255) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BYTEA NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (unique_key)\n)",
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE metadata (\n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (key)\n)",
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
"CREATE TABLE monitoring_sessions (\n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tlast_activity TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (session_id)\n)",
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
"CREATE UNIQUE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tstarted_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfinished_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE plugin_settings (\n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (plugin_author, plugin_name)\n)",
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
"CREATE TABLE users (\n\tid SERIAL NOT NULL, \n\t\"user\" VARCHAR(255) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at TIMESTAMP WITHOUT TIME ZONE, \n\tspace_api_key VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id)\n)",
"CREATE TABLE webhooks (\n\tid SERIAL NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id)\n)"
]
}
}
@@ -0,0 +1,379 @@
{
"source_commit": "03854b5d33d8b66fec4c86a77714e0e7512a31bd",
"source_path": "src/langbot/pkg/entity/persistence/",
"revision": "0025_bot_plugin_processors",
"dialects": {
"sqlite": [
"CREATE TABLE agent_interaction (\n\tid INTEGER NOT NULL, \n\tinteraction_id VARCHAR(255) NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tbinding_id VARCHAR(255) NOT NULL, \n\trunner_id VARCHAR(255) NOT NULL, \n\tprocessor_type VARCHAR(50) NOT NULL, \n\tprocessor_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_id VARCHAR(255), \n\tstatus VARCHAR(50) NOT NULL, \n\trequest_json TEXT NOT NULL, \n\tdelivery_target_json TEXT, \n\tdelivery_result_json TEXT, \n\treplaces_interaction_id VARCHAR(255), \n\tcallback_token_hash VARCHAR(64) NOT NULL, \n\texpires_at DATETIME, \n\tsubmitted_at DATETIME, \n\tsubmission_json TEXT, \n\tstatus_reason TEXT, \n\tcreated_at DATETIME NOT NULL, \n\tupdated_at DATETIME NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_agent_interaction_run_interaction UNIQUE (run_id, interaction_id)\n)",
"CREATE INDEX ix_agent_interaction_processor_id ON agent_interaction (processor_id)",
"CREATE INDEX ix_agent_interaction_binding_id ON agent_interaction (binding_id)",
"CREATE INDEX ix_agent_interaction_processor_type ON agent_interaction (processor_type)",
"CREATE INDEX ix_agent_interaction_scope_status ON agent_interaction (bot_id, conversation_id, actor_id, status)",
"CREATE UNIQUE INDEX ix_agent_interaction_callback_token_hash ON agent_interaction (callback_token_hash)",
"CREATE INDEX ix_agent_interaction_expires_at ON agent_interaction (expires_at)",
"CREATE INDEX ix_agent_interaction_actor_id ON agent_interaction (actor_id)",
"CREATE INDEX ix_agent_interaction_processor_status ON agent_interaction (processor_type, processor_id, status)",
"CREATE INDEX ix_agent_interaction_runner_id ON agent_interaction (runner_id)",
"CREATE INDEX ix_agent_interaction_bot_id ON agent_interaction (bot_id)",
"CREATE INDEX ix_agent_interaction_status ON agent_interaction (status)",
"CREATE INDEX ix_agent_interaction_run_id ON agent_interaction (run_id)",
"CREATE INDEX ix_agent_interaction_conversation_id ON agent_interaction (conversation_id)",
"CREATE TABLE agent_run (\n\tid INTEGER NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tevent_id VARCHAR(255), \n\tagent_id VARCHAR(255), \n\tbinding_id VARCHAR(255), \n\trunner_id VARCHAR(255) NOT NULL, \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tbot_id VARCHAR(255), \n\tstatus VARCHAR(50) NOT NULL, \n\tstatus_reason TEXT, \n\tqueue_name VARCHAR(255), \n\tpriority INTEGER NOT NULL, \n\trequested_runtime_id VARCHAR(255), \n\tclaimed_by_runtime_id VARCHAR(255), \n\tclaim_token VARCHAR(255), \n\tclaim_lease_expires_at DATETIME, \n\tdispatch_attempts INTEGER NOT NULL, \n\tlast_claimed_at DATETIME, \n\tcreated_at DATETIME NOT NULL, \n\tstarted_at DATETIME, \n\tfinished_at DATETIME, \n\tupdated_at DATETIME NOT NULL, \n\tdeadline_at DATETIME, \n\tcancel_requested_at DATETIME, \n\tusage_json TEXT, \n\tcost_json TEXT, \n\tauthorization_json TEXT, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_agent_run_queue_claim ON agent_run (queue_name, status, priority, id)",
"CREATE INDEX ix_agent_run_binding_id ON agent_run (binding_id)",
"CREATE INDEX ix_agent_run_bot_id ON agent_run (bot_id)",
"CREATE INDEX ix_agent_run_requested_runtime_id ON agent_run (requested_runtime_id)",
"CREATE INDEX ix_agent_run_claim_lease_expires_at ON agent_run (claim_lease_expires_at)",
"CREATE INDEX ix_agent_run_runner_id ON agent_run (runner_id)",
"CREATE INDEX ix_agent_run_status ON agent_run (status)",
"CREATE INDEX ix_agent_run_event_id ON agent_run (event_id)",
"CREATE INDEX ix_agent_run_scope_status ON agent_run (bot_id, workspace_id, conversation_id, thread_id, status)",
"CREATE INDEX ix_agent_run_claimed_by_runtime_id ON agent_run (claimed_by_runtime_id)",
"CREATE INDEX ix_agent_run_conversation_id ON agent_run (conversation_id)",
"CREATE INDEX ix_agent_run_runner_status ON agent_run (runner_id, status)",
"CREATE INDEX ix_agent_run_queue_name ON agent_run (queue_name)",
"CREATE UNIQUE INDEX ix_agent_run_run_id ON agent_run (run_id)",
"CREATE INDEX ix_agent_run_claim_token ON agent_run (claim_token)",
"CREATE TABLE agent_runtime (\n\tid INTEGER NOT NULL, \n\truntime_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tdisplay_name VARCHAR(255), \n\tendpoint VARCHAR(1024), \n\tversion VARCHAR(255), \n\tcapabilities_json TEXT, \n\tlabels_json TEXT, \n\tmetadata_json TEXT, \n\tlast_heartbeat_at DATETIME, \n\theartbeat_deadline_at DATETIME, \n\tcreated_at DATETIME NOT NULL, \n\tupdated_at DATETIME NOT NULL, \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_agent_runtime_heartbeat_deadline_at ON agent_runtime (heartbeat_deadline_at)",
"CREATE INDEX ix_agent_runtime_status ON agent_runtime (status)",
"CREATE UNIQUE INDEX ix_agent_runtime_runtime_id ON agent_runtime (runtime_id)",
"CREATE INDEX ix_agent_runtime_last_heartbeat_at ON agent_runtime (last_heartbeat_at)",
"CREATE TABLE agent_run_event (\n\tid INTEGER NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tsequence INTEGER NOT NULL, \n\ttype VARCHAR(100) NOT NULL, \n\tdata_json TEXT, \n\tusage_json TEXT, \n\tcreated_at DATETIME NOT NULL, \n\tsource VARCHAR(50), \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_agent_run_event_run_sequence UNIQUE (run_id, sequence)\n)",
"CREATE INDEX ix_agent_run_event_run_id ON agent_run_event (run_id)",
"CREATE INDEX ix_agent_run_event_run_sequence ON agent_run_event (run_id, sequence)",
"CREATE INDEX ix_agent_run_event_type ON agent_run_event (type)",
"CREATE TABLE directory_projection_states (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tcursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_coverage_cursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_fingerprint TEXT NOT NULL, \n\tlast_applied_at DATETIME NOT NULL, \n\tlease_expires_at DATETIME, \n\tPRIMARY KEY (instance_uuid), \n\tCONSTRAINT ck_directory_projection_state_cursor CHECK (cursor >= 0), \n\tCONSTRAINT ck_directory_projection_state_snapshot_coverage CHECK (snapshot_coverage_cursor >= 0 AND snapshot_coverage_cursor <= cursor), \n\tCONSTRAINT ck_directory_projection_state_fingerprint CHECK (length(snapshot_fingerprint) = 64)\n)",
"CREATE TABLE directory_projection_inbox (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tevent_uuid VARCHAR(36) NOT NULL, \n\tcursor BIGINT NOT NULL, \n\tevent_type VARCHAR(128) NOT NULL, \n\trevision BIGINT NOT NULL, \n\tfingerprint TEXT NOT NULL, \n\treceived_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tapplied_at DATETIME, \n\tPRIMARY KEY (instance_uuid, event_uuid), \n\tCONSTRAINT uq_directory_projection_inbox_cursor UNIQUE (instance_uuid, cursor), \n\tCONSTRAINT ck_directory_projection_inbox_cursor CHECK (cursor > 0), \n\tCONSTRAINT ck_directory_projection_inbox_revision CHECK (revision > 0), \n\tCONSTRAINT ck_directory_projection_inbox_fingerprint CHECK (length(fingerprint) = 64)\n)",
"CREATE INDEX ix_directory_projection_inbox_pending ON directory_projection_inbox (instance_uuid, applied_at, cursor)",
"CREATE TABLE event_log (\n\tid INTEGER NOT NULL, \n\tevent_id VARCHAR(255) NOT NULL, \n\tevent_type VARCHAR(100) NOT NULL, \n\tevent_time DATETIME, \n\tsource VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_type VARCHAR(50), \n\tactor_id VARCHAR(255), \n\tactor_name VARCHAR(255), \n\tsubject_type VARCHAR(50), \n\tsubject_id VARCHAR(255), \n\tinput_summary TEXT, \n\tinput_json TEXT, \n\traw_ref VARCHAR(255), \n\trun_id VARCHAR(255), \n\trunner_id VARCHAR(255), \n\tcreated_at DATETIME NOT NULL, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
"CREATE UNIQUE INDEX ix_event_log_event_id ON event_log (event_id)",
"CREATE INDEX ix_event_log_event_type ON event_log (event_type)",
"CREATE INDEX ix_event_log_conversation_id ON event_log (conversation_id)",
"CREATE INDEX ix_event_log_run_id ON event_log (run_id)",
"CREATE INDEX ix_event_log_bot_id ON event_log (bot_id)",
"CREATE TABLE metadata (\n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (\"key\")\n)",
"CREATE TABLE runner_state (\n\tid INTEGER NOT NULL, \n\trunner_id VARCHAR(255) NOT NULL, \n\tbinding_identity VARCHAR(255) NOT NULL, \n\tscope VARCHAR(50) NOT NULL, \n\tscope_key VARCHAR(512) NOT NULL, \n\tstate_key VARCHAR(255) NOT NULL, \n\tvalue_json TEXT, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_type VARCHAR(50), \n\tactor_id VARCHAR(255), \n\tsubject_type VARCHAR(50), \n\tsubject_id VARCHAR(255), \n\tcreated_at DATETIME NOT NULL, \n\tupdated_at DATETIME NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_runner_state_scope_key_state_key UNIQUE (scope_key, state_key)\n)",
"CREATE INDEX ix_runner_state_conversation_id ON runner_state (conversation_id)",
"CREATE INDEX ix_runner_state_scope ON runner_state (scope)",
"CREATE INDEX ix_runner_state_runner_id ON runner_state (runner_id)",
"CREATE INDEX ix_runner_state_actor_id ON runner_state (actor_id)",
"CREATE INDEX ix_runner_state_runner_binding ON runner_state (runner_id, binding_identity)",
"CREATE INDEX ix_runner_state_binding_identity ON runner_state (binding_identity)",
"CREATE INDEX ix_runner_state_bot_id ON runner_state (bot_id)",
"CREATE INDEX ix_runner_state_scope_key_lookup ON runner_state (scope_key)",
"CREATE TABLE transcript (\n\tid INTEGER NOT NULL, \n\ttranscript_id VARCHAR(255) NOT NULL, \n\tevent_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255) NOT NULL, \n\tthread_id VARCHAR(255), \n\trole VARCHAR(50) NOT NULL, \n\titem_type VARCHAR(50) NOT NULL, \n\tcontent TEXT, \n\tcontent_json TEXT, \n\tattachment_refs_json TEXT, \n\tseq INTEGER NOT NULL, \n\trun_id VARCHAR(255), \n\trunner_id VARCHAR(255), \n\tcreated_at DATETIME NOT NULL, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_transcript_conversation_id ON transcript (conversation_id)",
"CREATE UNIQUE INDEX ix_transcript_transcript_id ON transcript (transcript_id)",
"CREATE INDEX ix_transcript_event_id ON transcript (event_id)",
"CREATE INDEX ix_transcript_conversation_seq ON transcript (conversation_id, seq)",
"CREATE INDEX ix_transcript_bot_id ON transcript (bot_id)",
"CREATE INDEX ix_transcript_conversation_created ON transcript (conversation_id, created_at)",
"CREATE INDEX ix_transcript_scope_seq ON transcript (bot_id, workspace_id, conversation_id, thread_id, seq)",
"CREATE INDEX ix_transcript_run_id ON transcript (run_id)",
"CREATE TABLE users (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tuser VARCHAR(255) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at DATETIME, \n\tspace_api_key VARCHAR(255), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_users_normalized_email CHECK (normalized_email = trim(normalized_email) AND length(normalized_email) > 0 AND length(normalized_email) <= 320), \n\tCONSTRAINT ck_users_status CHECK (status IN ('active', 'disabled', 'deleted')), \n\tCONSTRAINT ck_users_source CHECK (source IN ('local', 'cloud_projection'))\n)",
"CREATE UNIQUE INDEX uq_users_normalized_email ON users (normalized_email)",
"CREATE UNIQUE INDEX uq_users_uuid ON users (uuid)",
"CREATE TABLE workspaces (\n\tuuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tslug VARCHAR(255) NOT NULL, \n\ttype VARCHAR(32) DEFAULT 'team' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspaces_instance_slug UNIQUE (instance_uuid, slug), \n\tCONSTRAINT ck_workspaces_type CHECK (type IN ('personal', 'team')), \n\tCONSTRAINT ck_workspaces_status CHECK (status IN ('provisioning', 'active', 'suspended', 'archived', 'deleted')), \n\tCONSTRAINT ck_workspaces_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE INDEX ix_workspaces_instance_status ON workspaces (instance_uuid, status)",
"CREATE UNIQUE INDEX uq_workspaces_local_instance ON workspaces (instance_uuid) WHERE source = 'local'",
"CREATE TABLE agents (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tkind VARCHAR(50) NOT NULL, \n\tcomponent_ref VARCHAR(255), \n\tconfig JSON NOT NULL, \n\tsupported_event_patterns JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid)\n)",
"CREATE INDEX ix_agents_workspace_updated ON agents (workspace_uuid, updated_at)",
"CREATE INDEX ix_agents_workspace_name ON agents (workspace_uuid, name)",
"CREATE TABLE api_keys (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tname VARCHAR(255) NOT NULL, \n\tkey_hash VARCHAR(64) NOT NULL, \n\tscopes JSON DEFAULT '[]' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\texpires_at DATETIME, \n\tlast_used_at DATETIME, \n\tdescription VARCHAR(512), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_api_keys_status CHECK (status IN ('active', 'revoked')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE UNIQUE INDEX uq_api_keys_uuid ON api_keys (uuid)",
"CREATE UNIQUE INDEX uq_api_keys_key_hash ON api_keys (key_hash)",
"CREATE INDEX ix_api_keys_workspace_status ON api_keys (workspace_uuid, status)",
"CREATE INDEX ix_api_keys_workspace_name ON api_keys (workspace_uuid, name)",
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tevent_bindings JSON DEFAULT '[]' NOT NULL, \n\tplugin_processors JSON DEFAULT '[]' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_bots_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_bots_workspace_name ON bots (workspace_uuid, name)",
"CREATE TABLE binary_storages (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tunique_key VARCHAR(255) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BLOB NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid, unique_key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_binary_storages_workspace_owner ON binary_storages (workspace_uuid, owner_type, owner)",
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\treadme TEXT DEFAULT '' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_mcp_servers_workspace_name UNIQUE (workspace_uuid, name), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_mcp_servers_workspace_enable ON mcp_servers (workspace_uuid, enable)",
"CREATE TABLE workspace_metadata (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, \"key\"), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_model_providers_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_model_providers_workspace_name ON model_providers (workspace_uuid, name)",
"CREATE INDEX ix_model_providers_workspace_requester ON model_providers (workspace_uuid, requester)",
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
"CREATE INDEX ix_monitoring_messages_workspace_session ON monitoring_messages (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
"CREATE INDEX ix_monitoring_messages_workspace_timestamp ON monitoring_messages (workspace_uuid, timestamp)",
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_llm_calls_workspace_session ON monitoring_llm_calls (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
"CREATE INDEX ix_monitoring_llm_calls_workspace_timestamp ON monitoring_llm_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
"CREATE TABLE monitoring_tool_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\ttool_name VARCHAR(255) NOT NULL, \n\ttool_source VARCHAR(50) NOT NULL, \n\tduration INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\targuments TEXT, \n\tresult TEXT, \n\terror_message TEXT, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_tool_calls_pipeline_id ON monitoring_tool_calls (pipeline_id)",
"CREATE INDEX ix_monitoring_tool_calls_workspace_timestamp ON monitoring_tool_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_tool_calls_session_id ON monitoring_tool_calls (session_id)",
"CREATE INDEX ix_monitoring_tool_calls_workspace_session ON monitoring_tool_calls (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_tool_calls_timestamp ON monitoring_tool_calls (timestamp)",
"CREATE INDEX ix_monitoring_tool_calls_bot_id ON monitoring_tool_calls (bot_id)",
"CREATE INDEX ix_monitoring_tool_calls_message_id ON monitoring_tool_calls (message_id)",
"CREATE TABLE monitoring_sessions (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time DATETIME NOT NULL, \n\tlast_activity DATETIME NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, session_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
"CREATE INDEX ix_monitoring_sessions_workspace_activity ON monitoring_sessions (workspace_uuid, last_activity)",
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
"CREATE INDEX ix_monitoring_sessions_workspace_active ON monitoring_sessions (workspace_uuid, is_active)",
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_errors_workspace_timestamp ON monitoring_errors (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
"CREATE INDEX ix_monitoring_errors_workspace_session ON monitoring_errors (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_embedding_calls_workspace_timestamp ON monitoring_embedding_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
"CREATE INDEX ix_monitoring_embedding_calls_workspace_kb ON monitoring_embedding_calls (workspace_uuid, knowledge_base_id)",
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_monitoring_feedback_workspace_feedback_id UNIQUE (workspace_uuid, feedback_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
"CREATE INDEX ix_monitoring_feedback_workspace_session ON monitoring_feedback (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
"CREATE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
"CREATE INDEX ix_monitoring_feedback_workspace_timestamp ON monitoring_feedback (workspace_uuid, timestamp)",
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_legacy_pipelines_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_legacy_pipelines_workspace_name ON legacy_pipelines (workspace_uuid, name)",
"CREATE INDEX ix_legacy_pipelines_workspace_default ON legacy_pipelines (workspace_uuid, is_default)",
"CREATE TABLE plugin_settings (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tinstallation_uuid VARCHAR(36) NOT NULL, \n\tartifact_digest VARCHAR(64) NOT NULL, \n\truntime_revision INTEGER NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid, plugin_author, plugin_name), \n\tCONSTRAINT uq_plugin_settings_installation_uuid UNIQUE (installation_uuid), \n\tCONSTRAINT ck_plugin_settings_runtime_revision_positive CHECK (runtime_revision >= 1), \n\tCONSTRAINT ck_plugin_settings_artifact_digest_length CHECK (length(artifact_digest) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_plugin_settings_workspace_enabled ON plugin_settings (workspace_uuid, enabled)",
"CREATE UNIQUE INDEX ix_plugin_settings_workspace_installation ON plugin_settings (workspace_uuid, installation_uuid)",
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME, \n\tupdated_at DATETIME, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tlegacy_vector_collection BOOLEAN DEFAULT 0 NOT NULL, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tembedding_dimension INTEGER, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_bases_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT ck_knowledge_bases_embedding_dimension_positive CHECK (embedding_dimension IS NULL OR embedding_dimension > 0), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_bases_workspace_name ON knowledge_bases (workspace_uuid, name)",
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
"CREATE UNIQUE INDEX uq_knowledge_bases_workspace_collection ON knowledge_bases (workspace_uuid, collection_id) WHERE collection_id IS NOT NULL",
"CREATE TABLE support_admin_temporary_sessions (\n\tgrant_jti_hash VARCHAR(64) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tactor_account_uuid VARCHAR(36) NOT NULL, \n\tissued_at DATETIME NOT NULL, \n\texpires_at DATETIME NOT NULL, \n\trevoked_at DATETIME, \n\tlast_used_at DATETIME, \n\tPRIMARY KEY (grant_jti_hash), \n\tCONSTRAINT ck_support_admin_sessions_grant_jti_hash CHECK (length(grant_jti_hash) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_support_admin_sessions_workspace_expiry ON support_admin_temporary_sessions (workspace_uuid, expires_at)",
"CREATE TABLE webhooks (\n\tid INTEGER NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_webhooks_workspace_name ON webhooks (workspace_uuid, name)",
"CREATE TABLE workspace_memberships (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tinvited_by_account_uuid VARCHAR(36), \n\tjoined_at DATETIME, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspace_membership_account UNIQUE (workspace_uuid, account_uuid), \n\tCONSTRAINT ck_workspace_memberships_role CHECK (role IN ('owner', 'admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_memberships_status CHECK (status IN ('active', 'disabled', 'removed')), \n\tCONSTRAINT ck_workspace_memberships_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(invited_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE UNIQUE INDEX uq_workspace_memberships_one_active_owner ON workspace_memberships (workspace_uuid) WHERE role = 'owner' AND status = 'active'",
"CREATE INDEX ix_workspace_memberships_account_status ON workspace_memberships (account_uuid, status)",
"CREATE TABLE workspace_invitations (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\ttoken_hash VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'pending' NOT NULL, \n\texpires_at DATETIME NOT NULL, \n\taccepted_at DATETIME, \n\trevoked_at DATETIME, \n\tcreated_by_account_uuid VARCHAR(36) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT ck_workspace_invitations_role CHECK (role IN ('admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_invitations_status CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
"CREATE UNIQUE INDEX uq_workspace_invitations_token_hash ON workspace_invitations (token_hash)",
"CREATE UNIQUE INDEX uq_workspace_invitations_pending_email ON workspace_invitations (workspace_uuid, normalized_email) WHERE status = 'pending'",
"CREATE TABLE workspace_execution_states (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tactive_generation BIGINT DEFAULT '1' NOT NULL, \n\tstate VARCHAR(32) DEFAULT 'active' NOT NULL, \n\twrite_fenced BOOLEAN DEFAULT 0 NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tdesired_state_revision BIGINT DEFAULT '0' NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid), \n\tCONSTRAINT ck_workspace_execution_generation CHECK (active_generation > 0), \n\tCONSTRAINT ck_workspace_execution_state CHECK (state IN ('provisioning', 'active', 'migrating', 'draining', 'inactive')), \n\tCONSTRAINT ck_workspace_execution_source CHECK (source IN ('local', 'cloud')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_workspace_execution_states_instance_state ON workspace_execution_states (instance_uuid, state)",
"CREATE TABLE bot_admins (\n\tid INTEGER NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_uuid VARCHAR(255) NOT NULL, \n\tlauncher_type VARCHAR(64) NOT NULL, \n\tlauncher_id VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_bot_admin UNIQUE (workspace_uuid, bot_uuid, launcher_type, launcher_id), \n\tCONSTRAINT fk_bot_admins_workspace_bot FOREIGN KEY(workspace_uuid, bot_uuid) REFERENCES bots (workspace_uuid, uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\tcontext_length INTEGER, \n\treasoning_config JSON DEFAULT '{\"level\":\"provider_default\"}' NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_llm_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_llm_models_workspace_name ON llm_models (workspace_uuid, name)",
"CREATE INDEX ix_llm_models_workspace_provider ON llm_models (workspace_uuid, provider_uuid)",
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_embedding_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_embedding_models_workspace_name ON embedding_models (workspace_uuid, name)",
"CREATE INDEX ix_embedding_models_workspace_provider ON embedding_models (workspace_uuid, provider_uuid)",
"CREATE TABLE rerank_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_rerank_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_rerank_models_workspace_name ON rerank_models (workspace_uuid, name)",
"CREATE INDEX ix_rerank_models_workspace_provider ON rerank_models (workspace_uuid, provider_uuid)",
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tstarted_at DATETIME NOT NULL, \n\tfinished_at DATETIME NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_pipeline_run_records_workspace_pipeline FOREIGN KEY(workspace_uuid, pipeline_uuid) REFERENCES legacy_pipelines (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_pipeline_run_records_workspace_created ON pipeline_run_records (workspace_uuid, created_at)",
"CREATE INDEX ix_pipeline_run_records_workspace_pipeline ON pipeline_run_records (workspace_uuid, pipeline_uuid)",
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at DATETIME, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_base_files_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT fk_knowledge_base_files_workspace_kb FOREIGN KEY(workspace_uuid, kb_id) REFERENCES knowledge_bases (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_base_files_workspace_kb ON knowledge_base_files (workspace_uuid, kb_id)",
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_knowledge_base_chunks_workspace_file FOREIGN KEY(workspace_uuid, file_id) REFERENCES knowledge_base_files (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_base_chunks_workspace_file ON knowledge_base_chunks (workspace_uuid, file_id)"
],
"postgresql": [
"CREATE TABLE agent_interaction (\n\tid SERIAL NOT NULL, \n\tinteraction_id VARCHAR(255) NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tbinding_id VARCHAR(255) NOT NULL, \n\trunner_id VARCHAR(255) NOT NULL, \n\tprocessor_type VARCHAR(50) NOT NULL, \n\tprocessor_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_id VARCHAR(255), \n\tstatus VARCHAR(50) NOT NULL, \n\trequest_json TEXT NOT NULL, \n\tdelivery_target_json TEXT, \n\tdelivery_result_json TEXT, \n\treplaces_interaction_id VARCHAR(255), \n\tcallback_token_hash VARCHAR(64) NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE, \n\tsubmitted_at TIMESTAMP WITHOUT TIME ZONE, \n\tsubmission_json TEXT, \n\tstatus_reason TEXT, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_agent_interaction_run_interaction UNIQUE (run_id, interaction_id)\n)",
"CREATE INDEX ix_agent_interaction_processor_id ON agent_interaction (processor_id)",
"CREATE INDEX ix_agent_interaction_binding_id ON agent_interaction (binding_id)",
"CREATE INDEX ix_agent_interaction_processor_type ON agent_interaction (processor_type)",
"CREATE INDEX ix_agent_interaction_scope_status ON agent_interaction (bot_id, conversation_id, actor_id, status)",
"CREATE UNIQUE INDEX ix_agent_interaction_callback_token_hash ON agent_interaction (callback_token_hash)",
"CREATE INDEX ix_agent_interaction_expires_at ON agent_interaction (expires_at)",
"CREATE INDEX ix_agent_interaction_actor_id ON agent_interaction (actor_id)",
"CREATE INDEX ix_agent_interaction_processor_status ON agent_interaction (processor_type, processor_id, status)",
"CREATE INDEX ix_agent_interaction_runner_id ON agent_interaction (runner_id)",
"CREATE INDEX ix_agent_interaction_bot_id ON agent_interaction (bot_id)",
"CREATE INDEX ix_agent_interaction_status ON agent_interaction (status)",
"CREATE INDEX ix_agent_interaction_run_id ON agent_interaction (run_id)",
"CREATE INDEX ix_agent_interaction_conversation_id ON agent_interaction (conversation_id)",
"CREATE TABLE agent_run (\n\tid SERIAL NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tevent_id VARCHAR(255), \n\tagent_id VARCHAR(255), \n\tbinding_id VARCHAR(255), \n\trunner_id VARCHAR(255) NOT NULL, \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tbot_id VARCHAR(255), \n\tstatus VARCHAR(50) NOT NULL, \n\tstatus_reason TEXT, \n\tqueue_name VARCHAR(255), \n\tpriority INTEGER NOT NULL, \n\trequested_runtime_id VARCHAR(255), \n\tclaimed_by_runtime_id VARCHAR(255), \n\tclaim_token VARCHAR(255), \n\tclaim_lease_expires_at TIMESTAMP WITHOUT TIME ZONE, \n\tdispatch_attempts INTEGER NOT NULL, \n\tlast_claimed_at TIMESTAMP WITHOUT TIME ZONE, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tstarted_at TIMESTAMP WITHOUT TIME ZONE, \n\tfinished_at TIMESTAMP WITHOUT TIME ZONE, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tdeadline_at TIMESTAMP WITHOUT TIME ZONE, \n\tcancel_requested_at TIMESTAMP WITHOUT TIME ZONE, \n\tusage_json TEXT, \n\tcost_json TEXT, \n\tauthorization_json TEXT, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_agent_run_queue_claim ON agent_run (queue_name, status, priority, id)",
"CREATE INDEX ix_agent_run_binding_id ON agent_run (binding_id)",
"CREATE INDEX ix_agent_run_bot_id ON agent_run (bot_id)",
"CREATE INDEX ix_agent_run_requested_runtime_id ON agent_run (requested_runtime_id)",
"CREATE INDEX ix_agent_run_claim_lease_expires_at ON agent_run (claim_lease_expires_at)",
"CREATE INDEX ix_agent_run_runner_id ON agent_run (runner_id)",
"CREATE INDEX ix_agent_run_status ON agent_run (status)",
"CREATE INDEX ix_agent_run_event_id ON agent_run (event_id)",
"CREATE INDEX ix_agent_run_scope_status ON agent_run (bot_id, workspace_id, conversation_id, thread_id, status)",
"CREATE INDEX ix_agent_run_claimed_by_runtime_id ON agent_run (claimed_by_runtime_id)",
"CREATE INDEX ix_agent_run_conversation_id ON agent_run (conversation_id)",
"CREATE INDEX ix_agent_run_runner_status ON agent_run (runner_id, status)",
"CREATE INDEX ix_agent_run_queue_name ON agent_run (queue_name)",
"CREATE UNIQUE INDEX ix_agent_run_run_id ON agent_run (run_id)",
"CREATE INDEX ix_agent_run_claim_token ON agent_run (claim_token)",
"CREATE TABLE agent_runtime (\n\tid SERIAL NOT NULL, \n\truntime_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tdisplay_name VARCHAR(255), \n\tendpoint VARCHAR(1024), \n\tversion VARCHAR(255), \n\tcapabilities_json TEXT, \n\tlabels_json TEXT, \n\tmetadata_json TEXT, \n\tlast_heartbeat_at TIMESTAMP WITHOUT TIME ZONE, \n\theartbeat_deadline_at TIMESTAMP WITHOUT TIME ZONE, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_agent_runtime_heartbeat_deadline_at ON agent_runtime (heartbeat_deadline_at)",
"CREATE INDEX ix_agent_runtime_status ON agent_runtime (status)",
"CREATE UNIQUE INDEX ix_agent_runtime_runtime_id ON agent_runtime (runtime_id)",
"CREATE INDEX ix_agent_runtime_last_heartbeat_at ON agent_runtime (last_heartbeat_at)",
"CREATE TABLE agent_run_event (\n\tid SERIAL NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tsequence INTEGER NOT NULL, \n\ttype VARCHAR(100) NOT NULL, \n\tdata_json TEXT, \n\tusage_json TEXT, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tsource VARCHAR(50), \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_agent_run_event_run_sequence UNIQUE (run_id, sequence)\n)",
"CREATE INDEX ix_agent_run_event_run_id ON agent_run_event (run_id)",
"CREATE INDEX ix_agent_run_event_run_sequence ON agent_run_event (run_id, sequence)",
"CREATE INDEX ix_agent_run_event_type ON agent_run_event (type)",
"CREATE TABLE directory_projection_states (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tcursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_coverage_cursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_fingerprint TEXT NOT NULL, \n\tlast_applied_at TIMESTAMP WITH TIME ZONE NOT NULL, \n\tlease_expires_at TIMESTAMP WITH TIME ZONE, \n\tPRIMARY KEY (instance_uuid), \n\tCONSTRAINT ck_directory_projection_state_cursor CHECK (cursor >= 0), \n\tCONSTRAINT ck_directory_projection_state_snapshot_coverage CHECK (snapshot_coverage_cursor >= 0 AND snapshot_coverage_cursor <= cursor), \n\tCONSTRAINT ck_directory_projection_state_fingerprint CHECK (length(snapshot_fingerprint) = 64)\n)",
"CREATE TABLE directory_projection_inbox (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tevent_uuid VARCHAR(36) NOT NULL, \n\tcursor BIGINT NOT NULL, \n\tevent_type VARCHAR(128) NOT NULL, \n\trevision BIGINT NOT NULL, \n\tfingerprint TEXT NOT NULL, \n\treceived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, \n\tapplied_at TIMESTAMP WITH TIME ZONE, \n\tPRIMARY KEY (instance_uuid, event_uuid), \n\tCONSTRAINT uq_directory_projection_inbox_cursor UNIQUE (instance_uuid, cursor), \n\tCONSTRAINT ck_directory_projection_inbox_cursor CHECK (cursor > 0), \n\tCONSTRAINT ck_directory_projection_inbox_revision CHECK (revision > 0), \n\tCONSTRAINT ck_directory_projection_inbox_fingerprint CHECK (length(fingerprint) = 64)\n)",
"CREATE INDEX ix_directory_projection_inbox_pending ON directory_projection_inbox (instance_uuid, applied_at, cursor)",
"CREATE TABLE event_log (\n\tid SERIAL NOT NULL, \n\tevent_id VARCHAR(255) NOT NULL, \n\tevent_type VARCHAR(100) NOT NULL, \n\tevent_time TIMESTAMP WITHOUT TIME ZONE, \n\tsource VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_type VARCHAR(50), \n\tactor_id VARCHAR(255), \n\tactor_name VARCHAR(255), \n\tsubject_type VARCHAR(50), \n\tsubject_id VARCHAR(255), \n\tinput_summary TEXT, \n\tinput_json TEXT, \n\traw_ref VARCHAR(255), \n\trun_id VARCHAR(255), \n\trunner_id VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
"CREATE UNIQUE INDEX ix_event_log_event_id ON event_log (event_id)",
"CREATE INDEX ix_event_log_event_type ON event_log (event_type)",
"CREATE INDEX ix_event_log_conversation_id ON event_log (conversation_id)",
"CREATE INDEX ix_event_log_run_id ON event_log (run_id)",
"CREATE INDEX ix_event_log_bot_id ON event_log (bot_id)",
"CREATE TABLE metadata (\n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (key)\n)",
"CREATE TABLE runner_state (\n\tid SERIAL NOT NULL, \n\trunner_id VARCHAR(255) NOT NULL, \n\tbinding_identity VARCHAR(255) NOT NULL, \n\tscope VARCHAR(50) NOT NULL, \n\tscope_key VARCHAR(512) NOT NULL, \n\tstate_key VARCHAR(255) NOT NULL, \n\tvalue_json TEXT, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_type VARCHAR(50), \n\tactor_id VARCHAR(255), \n\tsubject_type VARCHAR(50), \n\tsubject_id VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_runner_state_scope_key_state_key UNIQUE (scope_key, state_key)\n)",
"CREATE INDEX ix_runner_state_conversation_id ON runner_state (conversation_id)",
"CREATE INDEX ix_runner_state_scope ON runner_state (scope)",
"CREATE INDEX ix_runner_state_runner_id ON runner_state (runner_id)",
"CREATE INDEX ix_runner_state_actor_id ON runner_state (actor_id)",
"CREATE INDEX ix_runner_state_runner_binding ON runner_state (runner_id, binding_identity)",
"CREATE INDEX ix_runner_state_binding_identity ON runner_state (binding_identity)",
"CREATE INDEX ix_runner_state_bot_id ON runner_state (bot_id)",
"CREATE INDEX ix_runner_state_scope_key_lookup ON runner_state (scope_key)",
"CREATE TABLE transcript (\n\tid SERIAL NOT NULL, \n\ttranscript_id VARCHAR(255) NOT NULL, \n\tevent_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255) NOT NULL, \n\tthread_id VARCHAR(255), \n\trole VARCHAR(50) NOT NULL, \n\titem_type VARCHAR(50) NOT NULL, \n\tcontent TEXT, \n\tcontent_json TEXT, \n\tattachment_refs_json TEXT, \n\tseq INTEGER NOT NULL, \n\trun_id VARCHAR(255), \n\trunner_id VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
"CREATE INDEX ix_transcript_conversation_id ON transcript (conversation_id)",
"CREATE UNIQUE INDEX ix_transcript_transcript_id ON transcript (transcript_id)",
"CREATE INDEX ix_transcript_event_id ON transcript (event_id)",
"CREATE INDEX ix_transcript_conversation_seq ON transcript (conversation_id, seq)",
"CREATE INDEX ix_transcript_bot_id ON transcript (bot_id)",
"CREATE INDEX ix_transcript_conversation_created ON transcript (conversation_id, created_at)",
"CREATE INDEX ix_transcript_scope_seq ON transcript (bot_id, workspace_id, conversation_id, thread_id, seq)",
"CREATE INDEX ix_transcript_run_id ON transcript (run_id)",
"CREATE TABLE users (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\t\"user\" VARCHAR(255) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at TIMESTAMP WITHOUT TIME ZONE, \n\tspace_api_key VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_users_normalized_email CHECK (normalized_email = trim(normalized_email) AND length(normalized_email) > 0 AND length(normalized_email) <= 320), \n\tCONSTRAINT ck_users_status CHECK (status IN ('active', 'disabled', 'deleted')), \n\tCONSTRAINT ck_users_source CHECK (source IN ('local', 'cloud_projection'))\n)",
"CREATE UNIQUE INDEX uq_users_normalized_email ON users (normalized_email)",
"CREATE UNIQUE INDEX uq_users_uuid ON users (uuid)",
"CREATE TABLE workspaces (\n\tuuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tslug VARCHAR(255) NOT NULL, \n\ttype VARCHAR(32) DEFAULT 'team' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspaces_instance_slug UNIQUE (instance_uuid, slug), \n\tCONSTRAINT ck_workspaces_type CHECK (type IN ('personal', 'team')), \n\tCONSTRAINT ck_workspaces_status CHECK (status IN ('provisioning', 'active', 'suspended', 'archived', 'deleted')), \n\tCONSTRAINT ck_workspaces_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE INDEX ix_workspaces_instance_status ON workspaces (instance_uuid, status)",
"CREATE UNIQUE INDEX uq_workspaces_local_instance ON workspaces (instance_uuid) WHERE source = 'local'",
"CREATE TABLE agents (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tkind VARCHAR(50) NOT NULL, \n\tcomponent_ref VARCHAR(255), \n\tconfig JSON NOT NULL, \n\tsupported_event_patterns JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid)\n)",
"CREATE INDEX ix_agents_workspace_updated ON agents (workspace_uuid, updated_at)",
"CREATE INDEX ix_agents_workspace_name ON agents (workspace_uuid, name)",
"CREATE TABLE api_keys (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tname VARCHAR(255) NOT NULL, \n\tkey_hash VARCHAR(64) NOT NULL, \n\tscopes JSON DEFAULT '[]' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tdescription VARCHAR(512), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_api_keys_status CHECK (status IN ('active', 'revoked')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE UNIQUE INDEX uq_api_keys_uuid ON api_keys (uuid)",
"CREATE UNIQUE INDEX uq_api_keys_key_hash ON api_keys (key_hash)",
"CREATE INDEX ix_api_keys_workspace_status ON api_keys (workspace_uuid, status)",
"CREATE INDEX ix_api_keys_workspace_name ON api_keys (workspace_uuid, name)",
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tevent_bindings JSON DEFAULT '[]' NOT NULL, \n\tplugin_processors JSON DEFAULT '[]' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_bots_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_bots_workspace_name ON bots (workspace_uuid, name)",
"CREATE TABLE binary_storages (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tunique_key VARCHAR(255) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BYTEA NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid, unique_key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_binary_storages_workspace_owner ON binary_storages (workspace_uuid, owner_type, owner)",
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\treadme TEXT DEFAULT '' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_mcp_servers_workspace_name UNIQUE (workspace_uuid, name), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_mcp_servers_workspace_enable ON mcp_servers (workspace_uuid, enable)",
"CREATE TABLE workspace_metadata (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_model_providers_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_model_providers_workspace_name ON model_providers (workspace_uuid, name)",
"CREATE INDEX ix_model_providers_workspace_requester ON model_providers (workspace_uuid, requester)",
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
"CREATE INDEX ix_monitoring_messages_workspace_session ON monitoring_messages (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
"CREATE INDEX ix_monitoring_messages_workspace_timestamp ON monitoring_messages (workspace_uuid, timestamp)",
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_llm_calls_workspace_session ON monitoring_llm_calls (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
"CREATE INDEX ix_monitoring_llm_calls_workspace_timestamp ON monitoring_llm_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
"CREATE TABLE monitoring_tool_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\ttool_name VARCHAR(255) NOT NULL, \n\ttool_source VARCHAR(50) NOT NULL, \n\tduration INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\targuments TEXT, \n\tresult TEXT, \n\terror_message TEXT, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_tool_calls_pipeline_id ON monitoring_tool_calls (pipeline_id)",
"CREATE INDEX ix_monitoring_tool_calls_workspace_timestamp ON monitoring_tool_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_tool_calls_session_id ON monitoring_tool_calls (session_id)",
"CREATE INDEX ix_monitoring_tool_calls_workspace_session ON monitoring_tool_calls (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_tool_calls_timestamp ON monitoring_tool_calls (timestamp)",
"CREATE INDEX ix_monitoring_tool_calls_bot_id ON monitoring_tool_calls (bot_id)",
"CREATE INDEX ix_monitoring_tool_calls_message_id ON monitoring_tool_calls (message_id)",
"CREATE TABLE monitoring_sessions (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tlast_activity TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, session_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
"CREATE INDEX ix_monitoring_sessions_workspace_activity ON monitoring_sessions (workspace_uuid, last_activity)",
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
"CREATE INDEX ix_monitoring_sessions_workspace_active ON monitoring_sessions (workspace_uuid, is_active)",
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_errors_workspace_timestamp ON monitoring_errors (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
"CREATE INDEX ix_monitoring_errors_workspace_session ON monitoring_errors (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_embedding_calls_workspace_timestamp ON monitoring_embedding_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
"CREATE INDEX ix_monitoring_embedding_calls_workspace_kb ON monitoring_embedding_calls (workspace_uuid, knowledge_base_id)",
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_monitoring_feedback_workspace_feedback_id UNIQUE (workspace_uuid, feedback_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
"CREATE INDEX ix_monitoring_feedback_workspace_session ON monitoring_feedback (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
"CREATE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
"CREATE INDEX ix_monitoring_feedback_workspace_timestamp ON monitoring_feedback (workspace_uuid, timestamp)",
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_legacy_pipelines_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_legacy_pipelines_workspace_name ON legacy_pipelines (workspace_uuid, name)",
"CREATE INDEX ix_legacy_pipelines_workspace_default ON legacy_pipelines (workspace_uuid, is_default)",
"CREATE TABLE plugin_settings (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tinstallation_uuid VARCHAR(36) NOT NULL, \n\tartifact_digest VARCHAR(64) NOT NULL, \n\truntime_revision INTEGER NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid, plugin_author, plugin_name), \n\tCONSTRAINT uq_plugin_settings_installation_uuid UNIQUE (installation_uuid), \n\tCONSTRAINT ck_plugin_settings_runtime_revision_positive CHECK (runtime_revision >= 1), \n\tCONSTRAINT ck_plugin_settings_artifact_digest_length CHECK (length(artifact_digest) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_plugin_settings_workspace_enabled ON plugin_settings (workspace_uuid, enabled)",
"CREATE UNIQUE INDEX ix_plugin_settings_workspace_installation ON plugin_settings (workspace_uuid, installation_uuid)",
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tlegacy_vector_collection BOOLEAN DEFAULT false NOT NULL, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tembedding_dimension INTEGER, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_bases_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT ck_knowledge_bases_embedding_dimension_positive CHECK (embedding_dimension IS NULL OR embedding_dimension > 0), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_bases_workspace_name ON knowledge_bases (workspace_uuid, name)",
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
"CREATE UNIQUE INDEX uq_knowledge_bases_workspace_collection ON knowledge_bases (workspace_uuid, collection_id) WHERE collection_id IS NOT NULL",
"CREATE TABLE support_admin_temporary_sessions (\n\tgrant_jti_hash VARCHAR(64) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tactor_account_uuid VARCHAR(36) NOT NULL, \n\tissued_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\trevoked_at TIMESTAMP WITHOUT TIME ZONE, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tPRIMARY KEY (grant_jti_hash), \n\tCONSTRAINT ck_support_admin_sessions_grant_jti_hash CHECK (length(grant_jti_hash) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_support_admin_sessions_workspace_expiry ON support_admin_temporary_sessions (workspace_uuid, expires_at)",
"CREATE TABLE webhooks (\n\tid SERIAL NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_webhooks_workspace_name ON webhooks (workspace_uuid, name)",
"CREATE TABLE workspace_memberships (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tinvited_by_account_uuid VARCHAR(36), \n\tjoined_at TIMESTAMP WITHOUT TIME ZONE, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspace_membership_account UNIQUE (workspace_uuid, account_uuid), \n\tCONSTRAINT ck_workspace_memberships_role CHECK (role IN ('owner', 'admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_memberships_status CHECK (status IN ('active', 'disabled', 'removed')), \n\tCONSTRAINT ck_workspace_memberships_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(invited_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE UNIQUE INDEX uq_workspace_memberships_one_active_owner ON workspace_memberships (workspace_uuid) WHERE role = 'owner' AND status = 'active'",
"CREATE INDEX ix_workspace_memberships_account_status ON workspace_memberships (account_uuid, status)",
"CREATE TABLE workspace_invitations (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\ttoken_hash VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'pending' NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\taccepted_at TIMESTAMP WITHOUT TIME ZONE, \n\trevoked_at TIMESTAMP WITHOUT TIME ZONE, \n\tcreated_by_account_uuid VARCHAR(36) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT ck_workspace_invitations_role CHECK (role IN ('admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_invitations_status CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
"CREATE UNIQUE INDEX uq_workspace_invitations_token_hash ON workspace_invitations (token_hash)",
"CREATE UNIQUE INDEX uq_workspace_invitations_pending_email ON workspace_invitations (workspace_uuid, normalized_email) WHERE status = 'pending'",
"CREATE TABLE workspace_execution_states (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tactive_generation BIGINT DEFAULT '1' NOT NULL, \n\tstate VARCHAR(32) DEFAULT 'active' NOT NULL, \n\twrite_fenced BOOLEAN DEFAULT false NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tdesired_state_revision BIGINT DEFAULT '0' NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid), \n\tCONSTRAINT ck_workspace_execution_generation CHECK (active_generation > 0), \n\tCONSTRAINT ck_workspace_execution_state CHECK (state IN ('provisioning', 'active', 'migrating', 'draining', 'inactive')), \n\tCONSTRAINT ck_workspace_execution_source CHECK (source IN ('local', 'cloud')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_workspace_execution_states_instance_state ON workspace_execution_states (instance_uuid, state)",
"CREATE TABLE bot_admins (\n\tid SERIAL NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_uuid VARCHAR(255) NOT NULL, \n\tlauncher_type VARCHAR(64) NOT NULL, \n\tlauncher_id VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_bot_admin UNIQUE (workspace_uuid, bot_uuid, launcher_type, launcher_id), \n\tCONSTRAINT fk_bot_admins_workspace_bot FOREIGN KEY(workspace_uuid, bot_uuid) REFERENCES bots (workspace_uuid, uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\tcontext_length INTEGER, \n\treasoning_config JSON DEFAULT '{\"level\":\"provider_default\"}' NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_llm_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_llm_models_workspace_name ON llm_models (workspace_uuid, name)",
"CREATE INDEX ix_llm_models_workspace_provider ON llm_models (workspace_uuid, provider_uuid)",
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_embedding_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_embedding_models_workspace_name ON embedding_models (workspace_uuid, name)",
"CREATE INDEX ix_embedding_models_workspace_provider ON embedding_models (workspace_uuid, provider_uuid)",
"CREATE TABLE rerank_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_rerank_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_rerank_models_workspace_name ON rerank_models (workspace_uuid, name)",
"CREATE INDEX ix_rerank_models_workspace_provider ON rerank_models (workspace_uuid, provider_uuid)",
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tstarted_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfinished_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_pipeline_run_records_workspace_pipeline FOREIGN KEY(workspace_uuid, pipeline_uuid) REFERENCES legacy_pipelines (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_pipeline_run_records_workspace_created ON pipeline_run_records (workspace_uuid, created_at)",
"CREATE INDEX ix_pipeline_run_records_workspace_pipeline ON pipeline_run_records (workspace_uuid, pipeline_uuid)",
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_base_files_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT fk_knowledge_base_files_workspace_kb FOREIGN KEY(workspace_uuid, kb_id) REFERENCES knowledge_bases (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_base_files_workspace_kb ON knowledge_base_files (workspace_uuid, kb_id)",
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_knowledge_base_chunks_workspace_file FOREIGN KEY(workspace_uuid, file_id) REFERENCES knowledge_base_files (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_base_chunks_workspace_file ON knowledge_base_chunks (workspace_uuid, file_id)"
]
}
}
@@ -0,0 +1,66 @@
"""Reproduce immutable historical ORM DDL snapshots without a database."""
from __future__ import annotations
import importlib
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import types
import sqlalchemy as sa
from langbot.pkg.utils import constants
ROOT = Path(__file__).resolve().parents[4]
OUTPUT = Path(__file__).resolve().parent
SOURCE = 'src/langbot/pkg/entity/persistence/'
REFS = {
'baseline': ('9cd3544d59600fcb88700d05e4b211f59ac00445', '0001_baseline'),
'master': ('9b7ba0d64708496ace30a82866f6dbc185f089dc', '0024_passkey_credentials'),
'beta': ('03854b5d33d8b66fec4c86a77714e0e7512a31bd', '0025_bot_plugin_processors'),
}
def git(*args: str) -> str:
return subprocess.check_output(['git', *args], cwd=ROOT, text=True)
def generate() -> None:
# Only the baseline module's initial_metadata seed list reads this removed
# constant. It does not affect any table, column, constraint or generated DDL.
constants.required_database_version = 25
for name, (commit, revision) in REFS.items():
paths = git('ls-tree', '-r', '--name-only', commit, SOURCE).splitlines()
with tempfile.TemporaryDirectory(prefix='langbot-historical-schema-') as directory:
package = Path(directory)
for path in paths:
if path.endswith('.py'):
(package / Path(path).name).write_text(git('show', f'{commit}:{path}'))
# Keep ...utils imports working but give each snapshot its own Base.
package_name = f'langbot.pkg.entity.historical_{name}'
module = types.ModuleType(package_name)
module.__path__ = [directory]
sys.modules[package_name] = module
for path in sorted(package.glob('*.py')):
importlib.import_module(f'{package_name}.{path.stem}')
metadata = importlib.import_module(f'{package_name}.base').Base.metadata
dialects = {}
for dialect in ('sqlite', 'postgresql'):
statements = []
engine = sa.create_mock_engine(
f'{dialect}://',
lambda sql, *args, **kwargs: statements.append(str(sql.compile(dialect=engine.dialect)).strip()),
)
metadata.create_all(engine)
dialects[dialect] = statements
payload = {'source_commit': commit, 'source_path': SOURCE, 'revision': revision, 'dialects': dialects}
(OUTPUT / f'{name}_schema.json').write_text(json.dumps(payload, indent=2) + '\n')
print(f'{name}: {commit}, {len(metadata.tables)} historical ORM tables')
if __name__ == '__main__':
generate()
@@ -0,0 +1,259 @@
{
"source_commit": "9b7ba0d64708496ace30a82866f6dbc185f089dc",
"source_path": "src/langbot/pkg/entity/persistence/",
"revision": "0024_passkey_credentials",
"dialects": {
"sqlite": [
"CREATE TABLE directory_projection_states (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tcursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_coverage_cursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_fingerprint TEXT NOT NULL, \n\tlast_applied_at DATETIME NOT NULL, \n\tlease_expires_at DATETIME, \n\tPRIMARY KEY (instance_uuid), \n\tCONSTRAINT ck_directory_projection_state_cursor CHECK (cursor >= 0), \n\tCONSTRAINT ck_directory_projection_state_snapshot_coverage CHECK (snapshot_coverage_cursor >= 0 AND snapshot_coverage_cursor <= cursor), \n\tCONSTRAINT ck_directory_projection_state_fingerprint CHECK (length(snapshot_fingerprint) = 64)\n)",
"CREATE TABLE directory_projection_inbox (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tevent_uuid VARCHAR(36) NOT NULL, \n\tcursor BIGINT NOT NULL, \n\tevent_type VARCHAR(128) NOT NULL, \n\trevision BIGINT NOT NULL, \n\tfingerprint TEXT NOT NULL, \n\treceived_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tapplied_at DATETIME, \n\tPRIMARY KEY (instance_uuid, event_uuid), \n\tCONSTRAINT uq_directory_projection_inbox_cursor UNIQUE (instance_uuid, cursor), \n\tCONSTRAINT ck_directory_projection_inbox_cursor CHECK (cursor > 0), \n\tCONSTRAINT ck_directory_projection_inbox_revision CHECK (revision > 0), \n\tCONSTRAINT ck_directory_projection_inbox_fingerprint CHECK (length(fingerprint) = 64)\n)",
"CREATE INDEX ix_directory_projection_inbox_pending ON directory_projection_inbox (instance_uuid, applied_at, cursor)",
"CREATE TABLE metadata (\n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (\"key\")\n)",
"CREATE TABLE users (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tuser VARCHAR(255) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at DATETIME, \n\tspace_api_key VARCHAR(255), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_users_normalized_email CHECK (normalized_email = trim(normalized_email) AND length(normalized_email) > 0 AND length(normalized_email) <= 320), \n\tCONSTRAINT ck_users_status CHECK (status IN ('active', 'disabled', 'deleted')), \n\tCONSTRAINT ck_users_source CHECK (source IN ('local', 'cloud_projection'))\n)",
"CREATE UNIQUE INDEX uq_users_uuid ON users (uuid)",
"CREATE UNIQUE INDEX uq_users_normalized_email ON users (normalized_email)",
"CREATE TABLE passkey_credentials (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tcredential_id VARCHAR(255) NOT NULL, \n\tpublic_key TEXT NOT NULL, \n\tsign_count INTEGER NOT NULL, \n\taaguid VARCHAR(64), \n\ttransports VARCHAR(255), \n\tbacked_up BOOLEAN NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tlast_used_at DATETIME, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
"CREATE UNIQUE INDEX uq_passkey_credentials_cred_id ON passkey_credentials (credential_id)",
"CREATE UNIQUE INDEX uq_passkey_credentials_uuid ON passkey_credentials (uuid)",
"CREATE INDEX ix_passkey_credentials_account ON passkey_credentials (account_uuid)",
"CREATE TABLE workspaces (\n\tuuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tslug VARCHAR(255) NOT NULL, \n\ttype VARCHAR(32) DEFAULT 'team' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspaces_instance_slug UNIQUE (instance_uuid, slug), \n\tCONSTRAINT ck_workspaces_type CHECK (type IN ('personal', 'team')), \n\tCONSTRAINT ck_workspaces_status CHECK (status IN ('provisioning', 'active', 'suspended', 'archived', 'deleted')), \n\tCONSTRAINT ck_workspaces_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE UNIQUE INDEX uq_workspaces_local_instance ON workspaces (instance_uuid) WHERE source = 'local'",
"CREATE INDEX ix_workspaces_instance_status ON workspaces (instance_uuid, status)",
"CREATE TABLE api_keys (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tname VARCHAR(255) NOT NULL, \n\tkey_hash VARCHAR(64) NOT NULL, \n\tscopes JSON DEFAULT '[]' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\texpires_at DATETIME, \n\tlast_used_at DATETIME, \n\tdescription VARCHAR(512), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_api_keys_status CHECK (status IN ('active', 'revoked')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE INDEX ix_api_keys_workspace_name ON api_keys (workspace_uuid, name)",
"CREATE UNIQUE INDEX uq_api_keys_key_hash ON api_keys (key_hash)",
"CREATE INDEX ix_api_keys_workspace_status ON api_keys (workspace_uuid, status)",
"CREATE UNIQUE INDEX uq_api_keys_uuid ON api_keys (uuid)",
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tuse_pipeline_name VARCHAR(255), \n\tuse_pipeline_uuid VARCHAR(255), \n\tpipeline_routing_rules JSON DEFAULT '[]' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_bots_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_bots_workspace_name ON bots (workspace_uuid, name)",
"CREATE TABLE binary_storages (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tunique_key VARCHAR(255) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BLOB NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid, unique_key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_binary_storages_workspace_owner ON binary_storages (workspace_uuid, owner_type, owner)",
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\treadme TEXT DEFAULT '' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_mcp_servers_workspace_name UNIQUE (workspace_uuid, name), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_mcp_servers_workspace_enable ON mcp_servers (workspace_uuid, enable)",
"CREATE TABLE workspace_metadata (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, \"key\"), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_model_providers_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_model_providers_workspace_name ON model_providers (workspace_uuid, name)",
"CREATE INDEX ix_model_providers_workspace_requester ON model_providers (workspace_uuid, requester)",
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
"CREATE INDEX ix_monitoring_messages_workspace_timestamp ON monitoring_messages (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
"CREATE INDEX ix_monitoring_messages_workspace_session ON monitoring_messages (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_llm_calls_workspace_session ON monitoring_llm_calls (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
"CREATE INDEX ix_monitoring_llm_calls_workspace_timestamp ON monitoring_llm_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
"CREATE TABLE monitoring_tool_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\ttool_name VARCHAR(255) NOT NULL, \n\ttool_source VARCHAR(50) NOT NULL, \n\tduration INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\targuments TEXT, \n\tresult TEXT, \n\terror_message TEXT, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_tool_calls_bot_id ON monitoring_tool_calls (bot_id)",
"CREATE INDEX ix_monitoring_tool_calls_message_id ON monitoring_tool_calls (message_id)",
"CREATE INDEX ix_monitoring_tool_calls_pipeline_id ON monitoring_tool_calls (pipeline_id)",
"CREATE INDEX ix_monitoring_tool_calls_workspace_timestamp ON monitoring_tool_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_tool_calls_session_id ON monitoring_tool_calls (session_id)",
"CREATE INDEX ix_monitoring_tool_calls_workspace_session ON monitoring_tool_calls (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_tool_calls_timestamp ON monitoring_tool_calls (timestamp)",
"CREATE TABLE monitoring_sessions (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time DATETIME NOT NULL, \n\tlast_activity DATETIME NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, bot_id, session_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
"CREATE INDEX ix_monitoring_sessions_workspace_activity ON monitoring_sessions (workspace_uuid, last_activity)",
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
"CREATE INDEX ix_monitoring_sessions_workspace_active ON monitoring_sessions (workspace_uuid, is_active)",
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_errors_workspace_timestamp ON monitoring_errors (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
"CREATE INDEX ix_monitoring_errors_workspace_session ON monitoring_errors (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_embedding_calls_workspace_timestamp ON monitoring_embedding_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
"CREATE INDEX ix_monitoring_embedding_calls_workspace_kb ON monitoring_embedding_calls (workspace_uuid, knowledge_base_id)",
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_monitoring_feedback_workspace_feedback_id UNIQUE (workspace_uuid, feedback_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_feedback_workspace_timestamp ON monitoring_feedback (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
"CREATE INDEX ix_monitoring_feedback_workspace_session ON monitoring_feedback (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
"CREATE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_legacy_pipelines_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_legacy_pipelines_workspace_name ON legacy_pipelines (workspace_uuid, name)",
"CREATE INDEX ix_legacy_pipelines_workspace_default ON legacy_pipelines (workspace_uuid, is_default)",
"CREATE TABLE plugin_settings (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tinstallation_uuid VARCHAR(36) NOT NULL, \n\tartifact_digest VARCHAR(64) NOT NULL, \n\truntime_revision INTEGER NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid, plugin_author, plugin_name), \n\tCONSTRAINT uq_plugin_settings_installation_uuid UNIQUE (installation_uuid), \n\tCONSTRAINT ck_plugin_settings_runtime_revision_positive CHECK (runtime_revision >= 1), \n\tCONSTRAINT ck_plugin_settings_artifact_digest_length CHECK (length(artifact_digest) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_plugin_settings_workspace_enabled ON plugin_settings (workspace_uuid, enabled)",
"CREATE UNIQUE INDEX ix_plugin_settings_workspace_installation ON plugin_settings (workspace_uuid, installation_uuid)",
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME, \n\tupdated_at DATETIME, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tlegacy_vector_collection BOOLEAN DEFAULT 0 NOT NULL, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tembedding_dimension INTEGER, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_bases_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT ck_knowledge_bases_embedding_dimension_positive CHECK (embedding_dimension IS NULL OR embedding_dimension > 0), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE UNIQUE INDEX uq_knowledge_bases_workspace_collection ON knowledge_bases (workspace_uuid, collection_id) WHERE collection_id IS NOT NULL",
"CREATE INDEX ix_knowledge_bases_workspace_name ON knowledge_bases (workspace_uuid, name)",
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
"CREATE TABLE support_admin_temporary_sessions (\n\tgrant_jti_hash VARCHAR(64) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tactor_account_uuid VARCHAR(36) NOT NULL, \n\tissued_at DATETIME NOT NULL, \n\texpires_at DATETIME NOT NULL, \n\trevoked_at DATETIME, \n\tlast_used_at DATETIME, \n\tPRIMARY KEY (grant_jti_hash), \n\tCONSTRAINT ck_support_admin_sessions_grant_jti_hash CHECK (length(grant_jti_hash) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_support_admin_sessions_workspace_expiry ON support_admin_temporary_sessions (workspace_uuid, expires_at)",
"CREATE TABLE webhooks (\n\tid INTEGER NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_webhooks_workspace_name ON webhooks (workspace_uuid, name)",
"CREATE TABLE workspace_memberships (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tinvited_by_account_uuid VARCHAR(36), \n\tjoined_at DATETIME, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspace_membership_account UNIQUE (workspace_uuid, account_uuid), \n\tCONSTRAINT ck_workspace_memberships_role CHECK (role IN ('owner', 'admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_memberships_status CHECK (status IN ('active', 'disabled', 'removed')), \n\tCONSTRAINT ck_workspace_memberships_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(invited_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE INDEX ix_workspace_memberships_account_status ON workspace_memberships (account_uuid, status)",
"CREATE UNIQUE INDEX uq_workspace_memberships_one_active_owner ON workspace_memberships (workspace_uuid) WHERE role = 'owner' AND status = 'active'",
"CREATE TABLE workspace_invitations (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\ttoken_hash VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'pending' NOT NULL, \n\texpires_at DATETIME NOT NULL, \n\taccepted_at DATETIME, \n\trevoked_at DATETIME, \n\tcreated_by_account_uuid VARCHAR(36) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT ck_workspace_invitations_role CHECK (role IN ('admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_invitations_status CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
"CREATE UNIQUE INDEX uq_workspace_invitations_token_hash ON workspace_invitations (token_hash)",
"CREATE UNIQUE INDEX uq_workspace_invitations_pending_email ON workspace_invitations (workspace_uuid, normalized_email) WHERE status = 'pending'",
"CREATE TABLE workspace_execution_states (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tactive_generation BIGINT DEFAULT '1' NOT NULL, \n\tstate VARCHAR(32) DEFAULT 'active' NOT NULL, \n\twrite_fenced BOOLEAN DEFAULT 0 NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tdesired_state_revision BIGINT DEFAULT '0' NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid), \n\tCONSTRAINT ck_workspace_execution_generation CHECK (active_generation > 0), \n\tCONSTRAINT ck_workspace_execution_state CHECK (state IN ('provisioning', 'active', 'migrating', 'draining', 'inactive')), \n\tCONSTRAINT ck_workspace_execution_source CHECK (source IN ('local', 'cloud')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_workspace_execution_states_instance_state ON workspace_execution_states (instance_uuid, state)",
"CREATE TABLE bot_admins (\n\tid INTEGER NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_uuid VARCHAR(255) NOT NULL, \n\tlauncher_type VARCHAR(64) NOT NULL, \n\tlauncher_id VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_bot_admin UNIQUE (workspace_uuid, bot_uuid, launcher_type, launcher_id), \n\tCONSTRAINT fk_bot_admins_workspace_bot FOREIGN KEY(workspace_uuid, bot_uuid) REFERENCES bots (workspace_uuid, uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE TABLE codex_credentials (\n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpayload JSON NOT NULL, \n\tversion INTEGER NOT NULL, \n\tlease_owner VARCHAR(64), \n\tlease_until FLOAT NOT NULL, \n\tPRIMARY KEY (provider_uuid), \n\tCONSTRAINT fk_codex_credentials_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_codex_credentials_workspace ON codex_credentials (workspace_uuid)",
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\tcontext_length INTEGER, \n\treasoning_config JSON DEFAULT '{\"level\":\"provider_default\"}' NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_llm_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_llm_models_workspace_name ON llm_models (workspace_uuid, name)",
"CREATE INDEX ix_llm_models_workspace_provider ON llm_models (workspace_uuid, provider_uuid)",
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_embedding_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_embedding_models_workspace_name ON embedding_models (workspace_uuid, name)",
"CREATE INDEX ix_embedding_models_workspace_provider ON embedding_models (workspace_uuid, provider_uuid)",
"CREATE TABLE rerank_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_rerank_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_rerank_models_workspace_name ON rerank_models (workspace_uuid, name)",
"CREATE INDEX ix_rerank_models_workspace_provider ON rerank_models (workspace_uuid, provider_uuid)",
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tstarted_at DATETIME NOT NULL, \n\tfinished_at DATETIME NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_pipeline_run_records_workspace_pipeline FOREIGN KEY(workspace_uuid, pipeline_uuid) REFERENCES legacy_pipelines (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_pipeline_run_records_workspace_created ON pipeline_run_records (workspace_uuid, created_at)",
"CREATE INDEX ix_pipeline_run_records_workspace_pipeline ON pipeline_run_records (workspace_uuid, pipeline_uuid)",
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at DATETIME, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_base_files_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT fk_knowledge_base_files_workspace_kb FOREIGN KEY(workspace_uuid, kb_id) REFERENCES knowledge_bases (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_base_files_workspace_kb ON knowledge_base_files (workspace_uuid, kb_id)",
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_knowledge_base_chunks_workspace_file FOREIGN KEY(workspace_uuid, file_id) REFERENCES knowledge_base_files (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_base_chunks_workspace_file ON knowledge_base_chunks (workspace_uuid, file_id)"
],
"postgresql": [
"CREATE TABLE directory_projection_states (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tcursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_coverage_cursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_fingerprint TEXT NOT NULL, \n\tlast_applied_at TIMESTAMP WITH TIME ZONE NOT NULL, \n\tlease_expires_at TIMESTAMP WITH TIME ZONE, \n\tPRIMARY KEY (instance_uuid), \n\tCONSTRAINT ck_directory_projection_state_cursor CHECK (cursor >= 0), \n\tCONSTRAINT ck_directory_projection_state_snapshot_coverage CHECK (snapshot_coverage_cursor >= 0 AND snapshot_coverage_cursor <= cursor), \n\tCONSTRAINT ck_directory_projection_state_fingerprint CHECK (length(snapshot_fingerprint) = 64)\n)",
"CREATE TABLE directory_projection_inbox (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tevent_uuid VARCHAR(36) NOT NULL, \n\tcursor BIGINT NOT NULL, \n\tevent_type VARCHAR(128) NOT NULL, \n\trevision BIGINT NOT NULL, \n\tfingerprint TEXT NOT NULL, \n\treceived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, \n\tapplied_at TIMESTAMP WITH TIME ZONE, \n\tPRIMARY KEY (instance_uuid, event_uuid), \n\tCONSTRAINT uq_directory_projection_inbox_cursor UNIQUE (instance_uuid, cursor), \n\tCONSTRAINT ck_directory_projection_inbox_cursor CHECK (cursor > 0), \n\tCONSTRAINT ck_directory_projection_inbox_revision CHECK (revision > 0), \n\tCONSTRAINT ck_directory_projection_inbox_fingerprint CHECK (length(fingerprint) = 64)\n)",
"CREATE INDEX ix_directory_projection_inbox_pending ON directory_projection_inbox (instance_uuid, applied_at, cursor)",
"CREATE TABLE metadata (\n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (key)\n)",
"CREATE TABLE users (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\t\"user\" VARCHAR(255) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at TIMESTAMP WITHOUT TIME ZONE, \n\tspace_api_key VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_users_normalized_email CHECK (normalized_email = trim(normalized_email) AND length(normalized_email) > 0 AND length(normalized_email) <= 320), \n\tCONSTRAINT ck_users_status CHECK (status IN ('active', 'disabled', 'deleted')), \n\tCONSTRAINT ck_users_source CHECK (source IN ('local', 'cloud_projection'))\n)",
"CREATE UNIQUE INDEX uq_users_uuid ON users (uuid)",
"CREATE UNIQUE INDEX uq_users_normalized_email ON users (normalized_email)",
"CREATE TABLE passkey_credentials (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tcredential_id VARCHAR(255) NOT NULL, \n\tpublic_key TEXT NOT NULL, \n\tsign_count INTEGER NOT NULL, \n\taaguid VARCHAR(64), \n\ttransports VARCHAR(255), \n\tbacked_up BOOLEAN NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
"CREATE UNIQUE INDEX uq_passkey_credentials_cred_id ON passkey_credentials (credential_id)",
"CREATE UNIQUE INDEX uq_passkey_credentials_uuid ON passkey_credentials (uuid)",
"CREATE INDEX ix_passkey_credentials_account ON passkey_credentials (account_uuid)",
"CREATE TABLE workspaces (\n\tuuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tslug VARCHAR(255) NOT NULL, \n\ttype VARCHAR(32) DEFAULT 'team' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspaces_instance_slug UNIQUE (instance_uuid, slug), \n\tCONSTRAINT ck_workspaces_type CHECK (type IN ('personal', 'team')), \n\tCONSTRAINT ck_workspaces_status CHECK (status IN ('provisioning', 'active', 'suspended', 'archived', 'deleted')), \n\tCONSTRAINT ck_workspaces_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE UNIQUE INDEX uq_workspaces_local_instance ON workspaces (instance_uuid) WHERE source = 'local'",
"CREATE INDEX ix_workspaces_instance_status ON workspaces (instance_uuid, status)",
"CREATE TABLE api_keys (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tname VARCHAR(255) NOT NULL, \n\tkey_hash VARCHAR(64) NOT NULL, \n\tscopes JSON DEFAULT '[]' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tdescription VARCHAR(512), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_api_keys_status CHECK (status IN ('active', 'revoked')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE INDEX ix_api_keys_workspace_name ON api_keys (workspace_uuid, name)",
"CREATE UNIQUE INDEX uq_api_keys_key_hash ON api_keys (key_hash)",
"CREATE INDEX ix_api_keys_workspace_status ON api_keys (workspace_uuid, status)",
"CREATE UNIQUE INDEX uq_api_keys_uuid ON api_keys (uuid)",
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tuse_pipeline_name VARCHAR(255), \n\tuse_pipeline_uuid VARCHAR(255), \n\tpipeline_routing_rules JSON DEFAULT '[]' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_bots_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_bots_workspace_name ON bots (workspace_uuid, name)",
"CREATE TABLE binary_storages (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tunique_key VARCHAR(255) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BYTEA NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid, unique_key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_binary_storages_workspace_owner ON binary_storages (workspace_uuid, owner_type, owner)",
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\treadme TEXT DEFAULT '' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_mcp_servers_workspace_name UNIQUE (workspace_uuid, name), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_mcp_servers_workspace_enable ON mcp_servers (workspace_uuid, enable)",
"CREATE TABLE workspace_metadata (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_model_providers_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_model_providers_workspace_name ON model_providers (workspace_uuid, name)",
"CREATE INDEX ix_model_providers_workspace_requester ON model_providers (workspace_uuid, requester)",
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
"CREATE INDEX ix_monitoring_messages_workspace_timestamp ON monitoring_messages (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
"CREATE INDEX ix_monitoring_messages_workspace_session ON monitoring_messages (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_llm_calls_workspace_session ON monitoring_llm_calls (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
"CREATE INDEX ix_monitoring_llm_calls_workspace_timestamp ON monitoring_llm_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
"CREATE TABLE monitoring_tool_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\ttool_name VARCHAR(255) NOT NULL, \n\ttool_source VARCHAR(50) NOT NULL, \n\tduration INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\targuments TEXT, \n\tresult TEXT, \n\terror_message TEXT, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_tool_calls_bot_id ON monitoring_tool_calls (bot_id)",
"CREATE INDEX ix_monitoring_tool_calls_message_id ON monitoring_tool_calls (message_id)",
"CREATE INDEX ix_monitoring_tool_calls_pipeline_id ON monitoring_tool_calls (pipeline_id)",
"CREATE INDEX ix_monitoring_tool_calls_workspace_timestamp ON monitoring_tool_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_tool_calls_session_id ON monitoring_tool_calls (session_id)",
"CREATE INDEX ix_monitoring_tool_calls_workspace_session ON monitoring_tool_calls (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_tool_calls_timestamp ON monitoring_tool_calls (timestamp)",
"CREATE TABLE monitoring_sessions (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tlast_activity TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, bot_id, session_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
"CREATE INDEX ix_monitoring_sessions_workspace_activity ON monitoring_sessions (workspace_uuid, last_activity)",
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
"CREATE INDEX ix_monitoring_sessions_workspace_active ON monitoring_sessions (workspace_uuid, is_active)",
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_errors_workspace_timestamp ON monitoring_errors (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
"CREATE INDEX ix_monitoring_errors_workspace_session ON monitoring_errors (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_embedding_calls_workspace_timestamp ON monitoring_embedding_calls (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
"CREATE INDEX ix_monitoring_embedding_calls_workspace_kb ON monitoring_embedding_calls (workspace_uuid, knowledge_base_id)",
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_monitoring_feedback_workspace_feedback_id UNIQUE (workspace_uuid, feedback_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_monitoring_feedback_workspace_timestamp ON monitoring_feedback (workspace_uuid, timestamp)",
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
"CREATE INDEX ix_monitoring_feedback_workspace_session ON monitoring_feedback (workspace_uuid, session_id)",
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
"CREATE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_legacy_pipelines_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_legacy_pipelines_workspace_name ON legacy_pipelines (workspace_uuid, name)",
"CREATE INDEX ix_legacy_pipelines_workspace_default ON legacy_pipelines (workspace_uuid, is_default)",
"CREATE TABLE plugin_settings (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tinstallation_uuid VARCHAR(36) NOT NULL, \n\tartifact_digest VARCHAR(64) NOT NULL, \n\truntime_revision INTEGER NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid, plugin_author, plugin_name), \n\tCONSTRAINT uq_plugin_settings_installation_uuid UNIQUE (installation_uuid), \n\tCONSTRAINT ck_plugin_settings_runtime_revision_positive CHECK (runtime_revision >= 1), \n\tCONSTRAINT ck_plugin_settings_artifact_digest_length CHECK (length(artifact_digest) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_plugin_settings_workspace_enabled ON plugin_settings (workspace_uuid, enabled)",
"CREATE UNIQUE INDEX ix_plugin_settings_workspace_installation ON plugin_settings (workspace_uuid, installation_uuid)",
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tlegacy_vector_collection BOOLEAN DEFAULT false NOT NULL, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tembedding_dimension INTEGER, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_bases_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT ck_knowledge_bases_embedding_dimension_positive CHECK (embedding_dimension IS NULL OR embedding_dimension > 0), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE UNIQUE INDEX uq_knowledge_bases_workspace_collection ON knowledge_bases (workspace_uuid, collection_id) WHERE collection_id IS NOT NULL",
"CREATE INDEX ix_knowledge_bases_workspace_name ON knowledge_bases (workspace_uuid, name)",
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
"CREATE TABLE support_admin_temporary_sessions (\n\tgrant_jti_hash VARCHAR(64) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tactor_account_uuid VARCHAR(36) NOT NULL, \n\tissued_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\trevoked_at TIMESTAMP WITHOUT TIME ZONE, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tPRIMARY KEY (grant_jti_hash), \n\tCONSTRAINT ck_support_admin_sessions_grant_jti_hash CHECK (length(grant_jti_hash) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_support_admin_sessions_workspace_expiry ON support_admin_temporary_sessions (workspace_uuid, expires_at)",
"CREATE TABLE webhooks (\n\tid SERIAL NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_webhooks_workspace_name ON webhooks (workspace_uuid, name)",
"CREATE TABLE workspace_memberships (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tinvited_by_account_uuid VARCHAR(36), \n\tjoined_at TIMESTAMP WITHOUT TIME ZONE, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspace_membership_account UNIQUE (workspace_uuid, account_uuid), \n\tCONSTRAINT ck_workspace_memberships_role CHECK (role IN ('owner', 'admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_memberships_status CHECK (status IN ('active', 'disabled', 'removed')), \n\tCONSTRAINT ck_workspace_memberships_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(invited_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
"CREATE INDEX ix_workspace_memberships_account_status ON workspace_memberships (account_uuid, status)",
"CREATE UNIQUE INDEX uq_workspace_memberships_one_active_owner ON workspace_memberships (workspace_uuid) WHERE role = 'owner' AND status = 'active'",
"CREATE TABLE workspace_invitations (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\ttoken_hash VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'pending' NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\taccepted_at TIMESTAMP WITHOUT TIME ZONE, \n\trevoked_at TIMESTAMP WITHOUT TIME ZONE, \n\tcreated_by_account_uuid VARCHAR(36) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT ck_workspace_invitations_role CHECK (role IN ('admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_invitations_status CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
"CREATE UNIQUE INDEX uq_workspace_invitations_token_hash ON workspace_invitations (token_hash)",
"CREATE UNIQUE INDEX uq_workspace_invitations_pending_email ON workspace_invitations (workspace_uuid, normalized_email) WHERE status = 'pending'",
"CREATE TABLE workspace_execution_states (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tactive_generation BIGINT DEFAULT '1' NOT NULL, \n\tstate VARCHAR(32) DEFAULT 'active' NOT NULL, \n\twrite_fenced BOOLEAN DEFAULT false NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tdesired_state_revision BIGINT DEFAULT '0' NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid), \n\tCONSTRAINT ck_workspace_execution_generation CHECK (active_generation > 0), \n\tCONSTRAINT ck_workspace_execution_state CHECK (state IN ('provisioning', 'active', 'migrating', 'draining', 'inactive')), \n\tCONSTRAINT ck_workspace_execution_source CHECK (source IN ('local', 'cloud')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_workspace_execution_states_instance_state ON workspace_execution_states (instance_uuid, state)",
"CREATE TABLE bot_admins (\n\tid SERIAL NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_uuid VARCHAR(255) NOT NULL, \n\tlauncher_type VARCHAR(64) NOT NULL, \n\tlauncher_id VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_bot_admin UNIQUE (workspace_uuid, bot_uuid, launcher_type, launcher_id), \n\tCONSTRAINT fk_bot_admins_workspace_bot FOREIGN KEY(workspace_uuid, bot_uuid) REFERENCES bots (workspace_uuid, uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE TABLE codex_credentials (\n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpayload JSON NOT NULL, \n\tversion INTEGER NOT NULL, \n\tlease_owner VARCHAR(64), \n\tlease_until FLOAT NOT NULL, \n\tPRIMARY KEY (provider_uuid), \n\tCONSTRAINT fk_codex_credentials_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_codex_credentials_workspace ON codex_credentials (workspace_uuid)",
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\tcontext_length INTEGER, \n\treasoning_config JSON DEFAULT '{\"level\":\"provider_default\"}' NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_llm_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_llm_models_workspace_name ON llm_models (workspace_uuid, name)",
"CREATE INDEX ix_llm_models_workspace_provider ON llm_models (workspace_uuid, provider_uuid)",
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_embedding_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_embedding_models_workspace_name ON embedding_models (workspace_uuid, name)",
"CREATE INDEX ix_embedding_models_workspace_provider ON embedding_models (workspace_uuid, provider_uuid)",
"CREATE TABLE rerank_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_rerank_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_rerank_models_workspace_name ON rerank_models (workspace_uuid, name)",
"CREATE INDEX ix_rerank_models_workspace_provider ON rerank_models (workspace_uuid, provider_uuid)",
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tstarted_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfinished_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_pipeline_run_records_workspace_pipeline FOREIGN KEY(workspace_uuid, pipeline_uuid) REFERENCES legacy_pipelines (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_pipeline_run_records_workspace_created ON pipeline_run_records (workspace_uuid, created_at)",
"CREATE INDEX ix_pipeline_run_records_workspace_pipeline ON pipeline_run_records (workspace_uuid, pipeline_uuid)",
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_base_files_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT fk_knowledge_base_files_workspace_kb FOREIGN KEY(workspace_uuid, kb_id) REFERENCES knowledge_bases (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_base_files_workspace_kb ON knowledge_base_files (workspace_uuid, kb_id)",
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_knowledge_base_chunks_workspace_file FOREIGN KEY(workspace_uuid, file_id) REFERENCES knowledge_base_files (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
"CREATE INDEX ix_knowledge_base_chunks_workspace_file ON knowledge_base_chunks (workspace_uuid, file_id)"
]
}
}
@@ -193,6 +193,22 @@ async def create_legacy_resource_schema(engine, *, instance_uuid: str) -> None:
sa.Column('message_id', sa.String(255), nullable=True),
)
# Include historical monitoring columns consumed by later migrations.
for table_name in ('monitoring_messages', 'monitoring_sessions'):
table = monitoring_tables[table_name]
for name, value in (('bot_name', 'bot'), ('pipeline_id', 'pipeline-1'), ('pipeline_name', 'pipeline')):
table.append_column(sa.Column(name, sa.String(255), nullable=False, default=value))
for name in ('platform', 'user_id', 'user_name'):
table.append_column(sa.Column(name, sa.String(255)))
if table_name == 'monitoring_messages':
table.append_column(sa.Column('bot_id', sa.String(255), nullable=False, default='bot-1'))
table.append_column(sa.Column('role', sa.String(50)))
else:
table.append_column(sa.Column('message_count', sa.Integer, nullable=False, default=1))
table.append_column(
sa.Column('start_time', sa.DateTime, nullable=False, default=datetime.datetime(2026, 1, 1))
)
now = datetime.datetime(2026, 1, 1)
async with engine.begin() as conn:
await conn.run_sync(metadata.create_all)
@@ -0,0 +1,445 @@
"""Deterministic OAuth tests using real SQLite CAS writes, never live credentials."""
import asyncio
import base64
import json
import time
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import pytest
import pytest_asyncio
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import create_async_engine
from langbot.pkg.api.http.authz import Permission
from langbot.pkg.api.http.context import PrincipalContext, PrincipalType, RequestContext, WorkspaceContext
from langbot.pkg.entity.persistence.model import CodexCredential
from langbot.pkg.persistence.alembic_runner import run_alembic_stamp, run_alembic_upgrade
from langbot.pkg.provider.modelmgr.codex_auth import CodexAuth, _tokens, validate_config
from langbot.pkg.workspace.errors import WorkspaceNotFoundError
def context(workspace='w', user='u', principal=PrincipalType.ACCOUNT, permitted=True):
return RequestContext(
'i',
0,
'r',
'user_token',
PrincipalContext(principal, account_uuid=user),
WorkspaceContext(
workspace, 'm', 'owner', frozenset({Permission.PROVIDER_SECRET_MANAGE} if permitted else set())
),
)
def jwt(**claims):
return 'test.' + base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=') + '.test'
def token_response(**extra):
return {
'access_token': jwt(**{'https://api.openai.com/auth': {'chatgpt_account_id': 'account'}}),
'refresh_token': 'refresh-secret',
'expires_in': 3600,
**extra,
}
@pytest_asyncio.fixture
async def auth(tmp_path):
engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "codex.db"}')
@sa.event.listens_for(engine.sync_engine, 'connect')
def foreign_keys(connection, _):
connection.execute('PRAGMA foreign_keys=ON')
async with engine.begin() as conn:
await conn.execute(
sa.text(
'CREATE TABLE model_providers (uuid VARCHAR(255) PRIMARY KEY, workspace_uuid VARCHAR(36) NOT NULL, requester TEXT, UNIQUE(workspace_uuid, uuid))'
)
)
await conn.execute(
sa.text(
"INSERT INTO model_providers VALUES ('p','w','openai-codex'), ('other','other','openai-codex'), ('api','w','openai-chat-completions')"
)
)
await run_alembic_stamp(engine, '0021_merge_reasoning_config')
await run_alembic_upgrade(engine, '0022_codex_credentials')
async def execute(statement):
async with engine.begin() as conn:
return await conn.execute(statement)
service = CodexAuth(SimpleNamespace(persistence_mgr=SimpleNamespace(execute_async=execute)))
service.engine = engine
await execute(
sa.insert(CodexCredential).values(provider_uuid='p', workspace_uuid='w', payload={}, version=0, lease_until=0)
)
try:
yield service
finally:
await engine.dispose()
async def seed(auth, payload):
await auth.ap.persistence_mgr.execute_async(sa.update(CodexCredential).values(payload=payload))
@pytest.mark.asyncio
async def test_migration_upgrade_repeat_fk_cascade(auth):
await run_alembic_upgrade(auth.engine, '0022_codex_credentials')
await run_alembic_stamp(auth.engine, '0021_merge_reasoning_config')
await run_alembic_upgrade(auth.engine, '0022_codex_credentials')
with pytest.raises(sa.exc.IntegrityError):
await auth.ap.persistence_mgr.execute_async(
sa.insert(CodexCredential).values(
provider_uuid='other', workspace_uuid='w', payload={}, version=0, lease_until=0
)
)
await auth.ap.persistence_mgr.execute_async(sa.text("DELETE FROM model_providers WHERE uuid='p'"))
assert await auth._read('w', 'p') is None
from langbot.pkg.persistence.alembic_runner import run_alembic_downgrade
await run_alembic_downgrade(auth.engine, '0021_merge_reasoning_config')
async with auth.engine.connect() as conn:
assert 'codex_credentials' not in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
await run_alembic_upgrade(auth.engine, '0022_codex_credentials')
async with auth.engine.connect() as conn:
assert 'codex_credentials' in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
@pytest.mark.asyncio
async def test_device_pacing_exchange_secrecy_and_user_binding(auth):
auth._post = AsyncMock(
side_effect=[
httpx.Response(200, json={'device_auth_id': 'device-secret', 'usercode': 'CODE', 'interval': '5'}),
httpx.Response(200, json={'authorization_code': 'code-secret', 'code_verifier': 'verifier-secret'}),
httpx.Response(200, json=token_response()),
]
)
start = await auth.start(context(), 'p')
assert set(start) == {'authorization_id', 'user_code', 'interval', 'expires_at', 'verification_uri'}
assert 'device-secret' not in json.dumps(start)
attempt = start['authorization_id']
with pytest.raises(WorkspaceNotFoundError):
await auth.poll(context(user='attacker'), 'p', attempt)
assert (await auth.poll(context(), 'p', attempt))['status'] == 'pending'
assert auth._post.await_count == 1
row = await auth._read('w', 'p')
row['payload']['pending']['next_poll_at'] = 0
await seed(auth, row['payload'])
assert await auth.poll(context(), 'p', attempt) == {'status': 'connected'}
assert await auth.poll(context(), 'p', attempt) == {'status': 'connected'}
exchange = auth._post.call_args.kwargs['data']
assert exchange['grant_type'] == 'authorization_code'
assert exchange['redirect_uri'] == 'https://auth.openai.com/deviceauth/callback'
assert exchange['code_verifier'] == 'verifier-secret'
status = await auth.status(context(), 'p')
assert set(status) == {'status', 'connected', 'expires_at'}
assert 'secret' not in json.dumps(status)
await auth.disconnect(context(), 'p')
assert (await auth._read('w', 'p'))['payload'] == {}
@pytest.mark.asyncio
@pytest.mark.parametrize(
'ctx,provider,error',
[
(context('other'), 'p', WorkspaceNotFoundError),
(context(principal=PrincipalType.API_KEY), 'p', ValueError),
(context(permitted=False), 'p', ValueError),
(context(), 'api', ValueError),
],
)
async def test_auth_tenant_principal_permission_guards(auth, ctx, provider, error):
auth._post = AsyncMock()
with pytest.raises(error):
await auth.start(ctx, provider)
auth._post.assert_not_called()
@pytest.mark.asyncio
async def test_refresh_cross_instance_single_flight_and_rotation(auth):
old = _tokens(token_response())
old['expires_at'] = 0
await seed(auth, {'tokens': old})
entered, release = asyncio.Event(), asyncio.Event()
async def refresh(*args, **kwargs):
entered.set()
await release.wait()
return httpx.Response(200, json=token_response(refresh_token='rotated-secret'))
auth._post = AsyncMock(side_effect=refresh)
other = CodexAuth(auth.ap)
other._post = auth._post
first = asyncio.create_task(auth.access('w', 'p'))
await entered.wait()
second = asyncio.create_task(other.access('w', 'p'))
release.set()
a, b = await asyncio.gather(first, second)
assert a == b
assert a['refresh_token'] == 'rotated-secret'
assert auth._post.await_count == 1
assert (await auth._read('w', 'p'))['payload']['tokens'] == a
@pytest.mark.asyncio
@pytest.mark.parametrize(
'status,error,invalid',
[
(400, 'invalid_grant', True),
(401, 'refresh_token_reused', True),
(429, 'limited', False),
(500, 'secret-upstream-body', False),
(403, 'permission_denied', False),
],
)
async def test_refresh_errors_are_safe_and_transient_preserves_tokens(auth, status, error, invalid):
old = _tokens(token_response())
old['expires_at'] = 0
await seed(auth, {'tokens': old})
auth._post = AsyncMock(
return_value=httpx.Response(status, json={'error': error, 'access_token': 'secret-upstream-body'})
)
with pytest.raises(ValueError) as caught:
await auth.access('w', 'p')
assert 'secret' not in str(caught.value)
payload = (await auth._read('w', 'p'))['payload']
assert bool(payload.get('invalid')) == invalid
assert ('tokens' not in payload) if invalid else payload['tokens'] == old
@pytest.mark.asyncio
@pytest.mark.parametrize('cancel', [False, True])
async def test_disconnect_or_cancel_fences_inflight_exchange(auth, cancel):
old = _tokens(token_response())
await seed(
auth,
{
'tokens': old,
'pending': {
'authorization_id': 'attempt',
'account_uuid': 'u',
'expires_at': time.time() + 100,
'next_poll_at': 0,
'interval': 5,
'device_auth_id': 'device',
'user_code': 'code',
},
},
)
entered, release = asyncio.Event(), asyncio.Event()
async def post(path, **kwargs):
if path.endswith('/token') and path != '/oauth/token':
return httpx.Response(200, json={'authorization_code': 'code', 'code_verifier': 'verifier'})
entered.set()
await release.wait()
return httpx.Response(200, json=token_response())
auth._post = post
task = asyncio.create_task(auth.poll(context(), 'p', 'attempt'))
await entered.wait()
if cancel:
await auth.cancel(context(), 'p', 'attempt')
else:
await auth.disconnect(context(), 'p')
release.set()
with pytest.raises(ValueError, match='cancelled or replaced'):
await task
payload = (await auth._read('w', 'p'))['payload']
assert payload == ({'tokens': old} if cancel else {})
@pytest.mark.asyncio
@pytest.mark.parametrize('status,interval', [(403, 5), (404, 5), (429, 10)])
async def test_device_pending_and_backoff(auth, status, interval):
await seed(
auth,
{
'pending': {
'authorization_id': 'attempt',
'account_uuid': 'u',
'expires_at': time.time() + 100,
'next_poll_at': 0,
'interval': 5,
'device_auth_id': 'device',
'user_code': 'code',
}
},
)
auth._post = AsyncMock(return_value=httpx.Response(status))
assert await auth.poll(context(), 'p', 'attempt') == {'status': 'pending', 'interval': interval}
assert await auth.poll(context(), 'p', 'attempt') == {'status': 'pending', 'interval': interval}
assert auth._post.await_count == 1
@pytest.mark.asyncio
async def test_device_replacement_expiry_and_idempotent_cancel(auth):
auth._post = AsyncMock(return_value=httpx.Response(200, json={'device_auth_id': 'device', 'user_code': 'CODE'}))
first = await auth.start(context(), 'p')
second = await auth.start(context(), 'p')
assert first['authorization_id'] != second['authorization_id']
assert await auth.poll(context(), 'p', first['authorization_id']) == {'status': 'expired'}
await auth.cancel(context(), 'p', first['authorization_id'])
payload = (await auth._read('w', 'p'))['payload']
assert payload['pending']['authorization_id'] == second['authorization_id']
payload['pending']['expires_at'] = 0
await seed(auth, payload)
assert await auth.poll(context(), 'p', second['authorization_id']) == {'status': 'expired'}
assert 'pending' not in (await auth._read('w', 'p'))['payload']
@pytest.mark.asyncio
async def test_device_accepts_issuer_iso_expiry(auth):
from datetime import datetime, timezone
expires = datetime.fromtimestamp(time.time() + 600, timezone.utc).isoformat().replace('+00:00', 'Z')
auth._post = AsyncMock(
return_value=httpx.Response(200, json={'device_auth_id': 'device', 'user_code': 'CODE', 'expires_at': expires})
)
result = await auth.start(context(), 'p')
assert time.time() < result['expires_at'] < time.time() + 900
@pytest.mark.asyncio
async def test_device_expires_in_fallback(auth):
auth._post = AsyncMock(
return_value=httpx.Response(200, json={'device_auth_id': 'device', 'user_code': 'CODE', 'expires_in': 60})
)
result = await auth.start(context(), 'p')
assert time.time() < result['expires_at'] <= time.time() + 60
@pytest.mark.asyncio
@pytest.mark.parametrize('cancel_reads_before_refresh', [False, True])
async def test_cancel_pending_relogin_waits_for_existing_refresh(auth, cancel_reads_before_refresh):
old = _tokens(token_response())
old['expires_at'] = 0
await seed(auth, {'tokens': old, 'pending': {'authorization_id': 'attempt', 'account_uuid': 'u'}})
entered, release, cancel_read = asyncio.Event(), asyncio.Event(), asyncio.Event()
async def refresh(*args, **kwargs):
entered.set()
await release.wait()
return httpx.Response(200, json=token_response(refresh_token='rotated-secret'))
other = CodexAuth(auth.ap)
original_read = other._read
async def read(workspace, provider):
row = await original_read(workspace, provider)
cancel_read.set()
if cancel_reads_before_refresh:
await entered.wait()
return row
other._read = read
auth._post = refresh
if cancel_reads_before_refresh:
cancelling = asyncio.create_task(other.cancel(context(), 'p', 'attempt'))
await cancel_read.wait()
refreshing = asyncio.create_task(auth.access('w', 'p'))
await entered.wait()
if not cancel_reads_before_refresh:
cancelling = asyncio.create_task(other.cancel(context(), 'p', 'attempt'))
await cancel_read.wait()
await asyncio.sleep(0.05)
try:
assert not cancelling.done(), 'Cancellation must not revoke the refresh lease'
finally:
release.set()
results = await asyncio.gather(refreshing, cancelling, return_exceptions=True)
assert not any(isinstance(result, Exception) for result in results)
payload = (await auth._read('w', 'p'))['payload']
assert payload['tokens']['refresh_token'] == 'rotated-secret'
assert 'pending' not in payload
@pytest.mark.asyncio
@pytest.mark.parametrize('operation', ['save', 'cancel', 'disconnect', 'acquire', 'release', 'read'])
async def test_credential_database_errors_never_expose_secrets(auth, operation):
import traceback
markers = ['ACCESS-MARKER', 'REFRESH-MARKER', 'DEVICE-MARKER', 'VERIFIER-MARKER']
payload = {
'tokens': {'access_token': markers[0], 'refresh_token': markers[1]},
'pending': {
'authorization_id': 'attempt',
'account_uuid': 'u',
'device_auth_id': markers[2],
'code_verifier': markers[3],
},
}
await seed(auth, payload)
if operation == 'read':
auth.ap.persistence_mgr.execute_async = AsyncMock(
side_effect=sa.exc.StatementError(
'failure', 'SELECT credentials', {'payload': payload}, RuntimeError(markers[0])
)
)
else:
column = 'payload' if operation in ('save', 'cancel', 'disconnect') else 'lease_owner'
condition = ' WHEN NEW.lease_owner IS NULL' if operation == 'release' else ''
# Trigger errors can themselves contain secrets, even for parameter-free writes.
await auth.ap.persistence_mgr.execute_async(
sa.text(
f'CREATE TRIGGER reject_write BEFORE UPDATE OF {column} ON codex_credentials{condition} '
f"BEGIN SELECT RAISE(ABORT, '{' '.join(markers)}'); END"
)
)
with pytest.raises(ValueError, match='credential storage') as caught:
if operation == 'save':
async with auth._lease('w', 'p') as owner:
await auth._save('w', 'p', owner, payload)
elif operation == 'cancel':
await auth.cancel(context(), 'p', 'attempt')
elif operation == 'disconnect':
await auth.disconnect(context(), 'p')
elif operation == 'read':
await auth._read('w', 'p')
else:
async with auth._lease('w', 'p'):
pass
rendered = ''.join(traceback.format_exception(caught.value))
assert all(marker not in rendered for marker in markers)
assert caught.value.__suppress_context__
@pytest.mark.asyncio
async def test_credential_serialization_failure_is_sanitized(auth):
import traceback
class Secret:
def __repr__(self):
return 'SERIALIZATION-SECRET'
with pytest.raises(ValueError, match='credential storage') as caught:
async with auth._lease('w', 'p') as owner:
await auth._save('w', 'p', owner, {'tokens': {'refresh_token': Secret()}})
assert 'SERIALIZATION-SECRET' not in ''.join(traceback.format_exception(caught.value))
assert caught.value.__suppress_context__
def test_token_refresh_fallback_and_config_validation():
old = _tokens(token_response())
refreshed = _tokens({'access_token': 'opaque-access', 'expires_in': 3600}, old)
assert refreshed['refresh_token'] == old['refresh_token']
assert refreshed['connection_id'] == old['connection_id']
for expiry in [float('nan'), float('inf'), -1, 'bad']:
with pytest.raises(ValueError):
_tokens(token_response(expires_in=expiry))
data = {'requester': 'openai-codex'}
validate_config(data)
assert data['api_keys'] == []
for update in [{'base_url': 'https://evil.invalid'}, {'api_keys': ['secret']}]:
with pytest.raises(ValueError):
validate_config({**data, **update})
ordinary = {'requester': 'openai-chat-completions', 'api_keys': ['key'], 'base_url': 'https://custom.invalid'}
before = dict(ordinary)
validate_config(ordinary)
assert ordinary == before
@@ -0,0 +1,377 @@
"""Upgrade real historical ORM schemas through both published migration heads.
The fixtures are DDL snapshots, not current metadata stamped as an old version.
Every ancestor migration runs before we seed branch-specific data and merge.
PostgreSQL cases use a unique task-owned schema; no existing tables are dropped.
"""
from __future__ import annotations
import datetime
import json
import os
from pathlib import Path
import uuid
import pytest
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import create_async_engine
from langbot.pkg.entity import persistence
from langbot.pkg.entity.persistence.base import Base
from langbot.pkg.persistence.alembic_runner import (
get_alembic_current,
get_alembic_head,
run_alembic_downgrade,
run_alembic_upgrade,
)
from langbot.pkg.utils import importutil
pytestmark = pytest.mark.integration
importutil.import_modules_in_pkg(persistence)
FIXTURES = Path(__file__).with_name('fixtures')
WORKSPACE = '41100000-0000-4000-8000-000000000001'
ACCOUNT = '41100000-0000-4000-8000-000000000002'
NOW = datetime.datetime(2026, 9, 1)
@pytest.fixture(params=['sqlite', pytest.param('postgresql', marks=pytest.mark.slow)])
async def convergence_engine(request, tmp_path):
if request.param == 'sqlite':
engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "convergence.db"}')
try:
yield engine
finally:
await engine.dispose()
return
url = os.environ.get('TEST_POSTGRES_URL')
if not url:
pytest.skip('TEST_POSTGRES_URL not set')
schema = f'lb_convergence_{uuid.uuid4().hex}'
admin = create_async_engine(url)
engine = create_async_engine(url, connect_args={'server_settings': {'search_path': f'{schema},public'}})
try:
async with admin.begin() as conn:
await conn.exec_driver_sql(f'CREATE SCHEMA {schema}')
yield engine
finally:
await engine.dispose()
async with admin.begin() as conn:
await conn.exec_driver_sql(f'DROP SCHEMA IF EXISTS {schema} CASCADE')
await admin.dispose()
async def _insert(conn, table_name, **values):
table = await conn.run_sync(lambda sync: sa.Table(table_name, sa.MetaData(), autoload_with=sync))
# Historical baseline tables predate Workspace identity and event bindings.
await conn.execute(table.insert().values(**{key: value for key, value in values.items() if key in table.c}))
async def _seed_common(engine):
async with engine.begin() as conn:
tables = await conn.run_sync(lambda sync: set(sa.inspect(sync).get_table_names()))
await _insert(conn, 'metadata', key='instance_uuid', value='migration-convergence')
await _insert(
conn, 'users', uuid=ACCOUNT, user='owner@example.com', normalized_email='owner@example.com', password='hash'
)
if 'workspaces' in tables:
await _insert(
conn,
'workspaces',
uuid=WORKSPACE,
instance_uuid='migration-convergence',
name='Fixture',
slug='fixture',
created_by_account_uuid=ACCOUNT,
)
await _insert(
conn,
'workspace_memberships',
uuid='41100000-0000-4000-8000-000000000003',
workspace_uuid=WORKSPACE,
account_uuid=ACCOUNT,
role='owner',
source='local',
)
await _insert(
conn,
'model_providers',
uuid='provider-1',
workspace_uuid=WORKSPACE,
name='Fixture provider',
requester='openai',
base_url='https://fixture.invalid',
api_keys=['fixture-key'],
)
await _insert(
conn,
'legacy_pipelines',
uuid='pipeline-1',
workspace_uuid=WORKSPACE,
name='Fixture pipeline',
description='preserve pipeline',
for_version='4',
is_default=True,
stages=[],
config={},
extensions_preferences={},
)
await _insert(
conn,
'bots',
uuid='bot-1',
workspace_uuid=WORKSPACE,
name='Fixture bot',
description='preserve bot',
adapter='fixture',
adapter_config={},
enable=False,
use_pipeline_uuid='pipeline-1',
event_bindings=[
{
'id': 'route-1',
'event_pattern': 'message.*',
'target_type': 'pipeline',
'target_uuid': 'pipeline-1',
'enabled': True,
}
],
plugin_processors=[{'processor_uuid': 'agent-1', 'enabled': True}],
)
for index, bot in enumerate(('bot-1', 'bot-2')):
await _insert(
conn,
'monitoring_messages',
id=f'message-{index}',
workspace_uuid=WORKSPACE,
timestamp=NOW,
bot_id=bot,
bot_name=bot,
pipeline_id='pipeline-1',
pipeline_name='Fixture pipeline',
message_content=f'preserve message {index}',
session_id='shared-session',
status='success',
level='info',
role='user',
)
await _insert(
conn,
'monitoring_sessions',
workspace_uuid=WORKSPACE,
session_id='shared-session',
bot_id='bot-1',
bot_name='bot-1',
pipeline_id='pipeline-1',
pipeline_name='Fixture pipeline',
message_count=1,
start_time=NOW,
last_activity=NOW,
is_active=True,
)
async def _seed_branch_data(engine, source):
async with engine.begin() as conn:
if source == 'master':
await _insert(
conn,
'passkey_credentials',
uuid='41100000-0000-4000-8000-000000000004',
account_uuid=ACCOUNT,
name='Fixture passkey',
credential_id='fixture-credential',
public_key='fixture-public-key',
sign_count=7,
backed_up=True,
)
await _insert(
conn,
'codex_credentials',
provider_uuid='provider-1',
workspace_uuid=WORKSPACE,
payload={'fixture': 'preserve-codex'},
version=3,
lease_until=0.0,
)
# Master already supports colliding bot sessions before this merge.
await _insert(
conn,
'monitoring_sessions',
workspace_uuid=WORKSPACE,
session_id='shared-session',
bot_id='bot-2',
bot_name='bot-2',
pipeline_id='pipeline-1',
pipeline_name='Fixture pipeline',
message_count=1,
start_time=NOW,
last_activity=NOW,
is_active=True,
)
if source == 'beta':
await _insert(
conn,
'agents',
uuid='agent-1',
workspace_uuid=WORKSPACE,
name='Fixture agent',
description='preserve agent',
kind='event_processor',
component_ref='fixture:Runner',
config={'fixture': 'preserve-agent'},
supported_event_patterns=['message.*'],
)
await _insert(
conn,
'runner_state',
runner_id='fixture:Runner',
binding_identity='agent-1',
scope='bot',
scope_key='fixture-scope',
state_key='fixture-state',
value_json='{"sentinel": 411}',
bot_id='bot-1',
workspace_id=WORKSPACE,
created_at=NOW,
updated_at=NOW,
)
async def _assert_merged_schema_and_data(engine, source):
assert await get_alembic_current(engine) == get_alembic_head()
async with engine.connect() as conn:
inspector_data = await conn.run_sync(
lambda sync: {
'tables': set(sa.inspect(sync).get_table_names()),
'bot_columns': {c['name'] for c in sa.inspect(sync).get_columns('bots')},
'agent_columns': {c['name'] for c in sa.inspect(sync).get_columns('agents')},
'session_pk': sa.inspect(sync).get_pk_constraint('monitoring_sessions')['constrained_columns'],
}
)
assert {'agents', 'runner_state', 'codex_credentials', 'passkey_credentials'} <= inspector_data['tables']
assert {'event_bindings', 'plugin_processors'} <= inspector_data['bot_columns']
assert 'enabled' not in inspector_data['agent_columns']
assert inspector_data['session_pk'] == ['workspace_uuid', 'bot_id', 'session_id']
assert await conn.scalar(sa.text('SELECT password FROM users')) == 'hash'
assert await conn.scalar(sa.text('SELECT COUNT(*) FROM workspace_memberships')) == 1
assert await conn.scalar(sa.text('SELECT name FROM model_providers')) == 'Fixture provider'
assert await conn.scalar(sa.text('SELECT description FROM legacy_pipelines')) == 'preserve pipeline'
assert (
await conn.execute(sa.text('SELECT message_content FROM monitoring_messages ORDER BY id'))
).scalars().all() == [
'preserve message 0',
'preserve message 1',
]
assert (
await conn.execute(sa.text('SELECT bot_id FROM monitoring_sessions ORDER BY bot_id'))
).scalars().all() == [
'bot-1',
'bot-2',
]
bots = await conn.run_sync(lambda sync: sa.Table('bots', sa.MetaData(), autoload_with=sync))
row = (await conn.execute(sa.select(bots.c.event_bindings, bots.c.plugin_processors))).one()
assert row.event_bindings[0]['target_uuid'] == 'pipeline-1'
if source == 'beta':
assert row.plugin_processors == [{'processor_uuid': 'agent-1', 'enabled': True}]
assert await conn.scalar(sa.text('SELECT value_json FROM runner_state')) == '{"sentinel": 411}'
assert await conn.scalar(sa.text('SELECT name FROM agents')) == 'Fixture agent'
if source == 'master':
assert await conn.scalar(sa.text('SELECT sign_count FROM passkey_credentials')) == 7
credentials = await conn.run_sync(
lambda sync: sa.Table('codex_credentials', sa.MetaData(), autoload_with=sync)
)
assert await conn.scalar(sa.select(credentials.c.payload)) == {'fixture': 'preserve-codex'}
if engine.dialect.name == 'postgresql':
rows = (
await conn.execute(
sa.text(
'SELECT c.relname, c.relrowsecurity, c.relforcerowsecurity, '
'EXISTS (SELECT 1 FROM pg_policy p WHERE p.polrelid = c.oid '
"AND p.polname = 'langbot_workspace_isolation') AS policy "
'FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace '
"WHERE n.nspname = current_schema() AND c.relname IN ('agents', 'codex_credentials', 'monitoring_sessions')"
)
)
).all()
assert len(rows) == 3
assert all(row.relrowsecurity and row.relforcerowsecurity and row.policy for row in rows)
else:
assert (await conn.exec_driver_sql('PRAGMA foreign_key_check')).all() == []
@pytest.mark.asyncio
@pytest.mark.parametrize('source', ['baseline', 'master', 'beta'])
async def test_historical_schema_and_populated_branch_upgrade(convergence_engine, source):
engine = convergence_engine
fixture = json.loads((FIXTURES / f'{source}_schema.json').read_text())
async with engine.begin() as conn:
for statement in fixture['dialects'][engine.dialect.name]:
await conn.exec_driver_sql(statement)
# Execute the published ancestors; never stamp an empty or current schema.
await run_alembic_upgrade(engine, fixture['revision'])
assert await get_alembic_current(engine) == fixture['revision']
async with engine.connect() as conn:
tables = await conn.run_sync(lambda sync: set(sa.inspect(sync).get_table_names()))
if source == 'master':
assert {'codex_credentials', 'passkey_credentials'} <= tables
assert 'agents' not in tables and 'runner_state' not in tables
elif source == 'beta':
assert {'agents', 'runner_state'} <= tables
assert 'codex_credentials' not in tables and 'passkey_credentials' not in tables
assert await conn.run_sync(
lambda sync: sa.inspect(sync).get_pk_constraint('monitoring_sessions')['constrained_columns']
) == ['workspace_uuid', 'session_id']
else:
assert 'workspaces' not in tables
await _seed_common(engine)
await _seed_branch_data(engine, source)
if source == 'baseline' and engine.dialect.name == 'postgresql':
# Match PersistenceManager's staged legacy bootstrap: expand the
# account/resource contract before creating deferred tenant tables.
# RLS 0011 requires those tables (including monitoring_tool_calls).
await run_alembic_upgrade(engine, '0010_scope_resources')
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await run_alembic_upgrade(engine, 'head')
await _assert_merged_schema_and_data(engine, source)
await run_alembic_upgrade(engine, 'head')
await _assert_merged_schema_and_data(engine, source)
@pytest.mark.asyncio
async def test_empty_database_startup_schema_then_real_migrations(convergence_engine):
engine = convergence_engine
async with engine.begin() as conn:
assert await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names()) == []
# This is the documented fresh-install contract: create_all precedes Alembic.
await conn.run_sync(Base.metadata.create_all)
await run_alembic_upgrade(engine, 'head')
assert await get_alembic_current(engine) == get_alembic_head()
@pytest.mark.asyncio
async def test_merge_only_downgrade_preserves_both_branch_schemas(convergence_engine):
engine = convergence_engine
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await run_alembic_upgrade(engine, 'head')
await _seed_common(engine)
await _seed_branch_data(engine, 'master')
await _seed_branch_data(engine, 'beta')
# Targeting a parent removes only the merge, leaving both parent heads.
# A relative -1 is ambiguous at a merge point. Neither feature is reverted.
await run_alembic_downgrade(engine, '0024_passkey_credentials')
async with engine.connect() as conn:
revisions = set((await conn.execute(sa.text('SELECT version_num FROM alembic_version'))).scalars())
knowledge_columns = await conn.run_sync(
lambda sync: {column['name'] for column in sa.inspect(sync).get_columns('knowledge_bases')}
)
assert revisions == {'0024_passkey_credentials', '0026_knowledge_base_drafts'}
assert 'initialized' in knowledge_columns
await run_alembic_upgrade(engine, 'head')
await _assert_merged_schema_and_data(engine, 'master')
await _assert_merged_schema_and_data(engine, 'beta')
@@ -64,6 +64,17 @@ async def sqlite_engine(sqlite_db_url):
await engine.dispose()
def test_migration_graph_has_one_head_containing_both_released_branches():
cfg = Config()
cfg.set_main_option('script_location', _ALEMBIC_DIR)
scripts = ScriptDirectory.from_config(cfg)
heads = scripts.get_heads()
assert len(heads) == 1, f'Release migrations must converge, found {heads}'
ancestors = {revision.revision for revision in scripts.walk_revisions()}
assert {'0024_passkey_credentials', '0025_bot_plugin_processors'} <= ancestors
assert all(len(revision) <= 32 for revision in ancestors)
class TestSQLiteMigrationBaseline:
"""Tests for baseline stamp workflow."""
@@ -313,6 +324,15 @@ class TestSQLiteMigrationUpgrade:
class TestSQLiteMigrationFreshDatabase:
"""Tests for fresh database workflow."""
@pytest.mark.asyncio
async def test_bot_scoped_sessions_skips_absent_table(self, sqlite_engine):
"""A partial schema needs no session key migration in either direction."""
await run_alembic_stamp(sqlite_engine, '0022_codex_credentials')
await run_alembic_upgrade(sqlite_engine, '0023_bot_scoped_sessions')
assert await get_alembic_current(sqlite_engine) == '0023_bot_scoped_sessions'
await run_alembic_downgrade(sqlite_engine, '0022_codex_credentials')
assert await get_alembic_current(sqlite_engine) == '0022_codex_credentials'
@pytest.mark.asyncio
async def test_fresh_db_upgrade_from_scratch(self, tmp_path):
"""
@@ -549,10 +549,14 @@ class TestPostgreSQLWorkspaceMigration:
await conn.run_sync(lambda sync_conn: sa.inspect(sync_conn).get_table_names())
)
assert 'workspaces' not in tables_before_migration
assert 'codex_credentials' not in tables_before_migration
assert 'passkey_credentials' not in tables_before_migration
await manager._initialize_managed_schema()
async with postgres_engine.connect() as conn:
assert 'codex_credentials' in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
assert 'passkey_credentials' in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
account = (await conn.execute(text('SELECT uuid, status, source FROM users'))).mappings().one()
workspace = (
(await conn.execute(text('SELECT * FROM workspaces WHERE source = :source'), {'source': 'local'}))
@@ -0,0 +1,354 @@
"""Monitoring regressions through asyncpg, Cloud UoW guards, and migrated RLS.
TEST_POSTGRES_URL must identify a disposable PostgreSQL/pgvector test server
with permission to create databases and roles. Each run owns a fresh database;
no existing tables are dropped. Without that URL these tests are skipped.
"""
from __future__ import annotations
import logging
import os
import uuid
from types import SimpleNamespace
import pytest
import pytest_asyncio
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import create_async_engine
from langbot.pkg.api.http.context import ExecutionContext
from langbot.pkg.api.http.service.monitoring import MonitoringService
from langbot.pkg.entity.persistence import monitoring as models
from langbot.pkg.entity.persistence.workspace import Workspace
from langbot.pkg.persistence.mgr import PersistenceManager, PersistenceMode
from langbot.pkg.persistence.tenant_uow import TenantScopeRequiredError
from langbot.pkg.pipeline.monitoring_helper import MonitoringHelper
pytestmark = [pytest.mark.integration, pytest.mark.slow, pytest.mark.asyncio(loop_scope='module')]
WORKSPACE_A = '00000000-0000-0000-0000-00000000000a'
WORKSPACE_B = '00000000-0000-0000-0000-00000000000b'
RESOURCE = dict(bot_id='same-bot', bot_name='Bot', pipeline_id='same-pipeline', pipeline_name='Pipeline')
MONITORING_TABLES = tuple(
table for table in models.MonitoringMessage.metadata.sorted_tables if table.name.startswith('monitoring_')
)
def _context(workspace_uuid):
return ExecutionContext(
instance_uuid='monitoring-postgres-test',
workspace_uuid=workspace_uuid,
placement_generation=1,
bot_uuid=RESOURCE['bot_id'],
pipeline_uuid=RESOURCE['pipeline_id'],
)
def _application(url):
return SimpleNamespace(
instance_config=SimpleNamespace(
data={
'database': {
'use': 'postgresql',
'postgresql': {
'host': url.host,
'port': url.port,
'user': url.username,
'password': url.password,
'database': url.database,
},
}
}
),
logger=logging.getLogger('monitoring-postgres-test'),
)
@pytest_asyncio.fixture(scope='module', loop_scope='module')
async def cloud_database():
url = os.environ.get('TEST_POSTGRES_URL')
if not url:
pytest.skip('TEST_POSTGRES_URL not set')
admin_url = sa.engine.make_url(url)
admin = create_async_engine(admin_url, isolation_level='AUTOCOMMIT')
suffix = uuid.uuid4().hex[:12]
database_name = f'lb_monitoring_{suffix}'
runtime_role = f'lb_monitoring_{suffix}'
password = f'Test{uuid.uuid4().hex}'
database_created = role_created = False
release_manager = runtime_manager = None
quote = admin.dialect.identifier_preparer.quote
from langbot.pkg.persistence import mgr as mgr_module
from langbot.pkg.persistence.databases.postgresql import PostgreSQLDatabaseManager
from langbot.pkg.utils import constants
with pytest.MonkeyPatch.context() as patch:
patch.setattr(mgr_module.database, 'preregistered_managers', [PostgreSQLDatabaseManager])
patch.setattr(constants, 'instance_id', 'monitoring-postgres-test')
try:
async with admin.connect() as conn:
await conn.execute(sa.text(f'CREATE DATABASE {quote(database_name)}'))
database_created = True
await conn.execute(
sa.text(f"CREATE ROLE {quote(runtime_role)} LOGIN NOSUPERUSER NOBYPASSRLS PASSWORD '{password}'")
)
role_created = True
release_app = _application(admin_url.set(database=database_name))
release_manager = PersistenceManager(release_app, mode=PersistenceMode.RELEASE_MIGRATION)
release_app.persistence_mgr = release_manager
await release_manager.initialize()
async with release_manager.get_db_engine().begin() as conn:
for workspace in (WORKSPACE_A, WORKSPACE_B):
await conn.execute(
sa.insert(Workspace).values(
uuid=workspace,
instance_uuid='monitoring-postgres-test',
name=workspace,
slug=workspace,
source='cloud_projection',
)
)
tables = release_manager._runtime_business_table_names()
quoted_tables = ', '.join(f'public.{quote(name)}' for name in tables)
await conn.execute(
sa.text(f'GRANT CONNECT ON DATABASE {quote(database_name)} TO {quote(runtime_role)}')
)
await conn.execute(sa.text(f'GRANT USAGE ON SCHEMA public TO {quote(runtime_role)}'))
await conn.execute(
sa.text(f'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE {quoted_tables} TO {quote(runtime_role)}')
)
await conn.execute(sa.text(f'GRANT SELECT ON public.alembic_version TO {quote(runtime_role)}'))
sequences = await release_manager._runtime_business_sequence_names(conn, tables)
if sequences:
names = ', '.join(f'public.{quote(name)}' for name in sequences)
await conn.execute(sa.text(f'GRANT USAGE, SELECT ON SEQUENCE {names} TO {quote(runtime_role)}'))
runtime_app = _application(admin_url.set(database=database_name, username=runtime_role, password=password))
runtime_manager = PersistenceManager(runtime_app, mode=PersistenceMode.CLOUD_RUNTIME)
runtime_app.persistence_mgr = runtime_manager
await runtime_manager.initialize()
runtime_app.monitoring_service = MonitoringService(runtime_app)
yield runtime_app, release_manager.get_db_engine()
finally:
if runtime_manager is not None:
await runtime_manager.shutdown()
if release_manager is not None:
await release_manager.shutdown()
async with admin.connect() as conn:
if database_created:
await conn.execute(sa.text(f'DROP DATABASE {quote(database_name)} WITH (FORCE)'))
if role_created:
await conn.execute(sa.text(f'DROP ROLE {quote(runtime_role)}'))
await admin.dispose()
@pytest_asyncio.fixture(loop_scope='module')
async def service(cloud_database):
application, admin = cloud_database
async with admin.begin() as conn:
for table in MONITORING_TABLES:
await conn.execute(sa.delete(table))
application.instance_config.data.pop('monitoring', None)
return application.monitoring_service
async def _read(service, method, context, *args, **kwargs):
# HTTP auth binds a tenant scope; exercise that same guard for service reads.
async with service.ap.persistence_mgr.tenant_scope(context.workspace_uuid):
return await getattr(service, method)(context, *args, **kwargs)
def _query(context, sender_id):
return SimpleNamespace(
_execution_context=context,
launcher_type='person',
launcher_id='same-user',
sender_id=sender_id,
message_chain=SimpleNamespace(model_dump=lambda: [{'type': 'Plain', 'text': 'hello'}]),
resp_message_chain=[SimpleNamespace(model_dump=lambda: [{'type': 'Plain', 'text': 'reply'}])],
message_event=SimpleNamespace(sender=SimpleNamespace(nickname='Alice')),
variables={'public': 'value', '_private': 'hidden'},
)
@pytest.mark.parametrize('user_id', [123456789, -100123456789, 0, None, '', '00123', ' opaque用户 '])
@pytest.mark.parametrize('record_type', ['message', 'session', 'feedback'])
async def test_optional_user_ids_round_trip_through_asyncpg(service, user_id, record_type):
context = _context(WORKSPACE_A)
expected = str(user_id) if isinstance(user_id, int) else user_id
if record_type == 'message':
record_id = await service.record_message(
context,
**RESOURCE,
message_content='hello',
session_id='same-session',
user_id=user_id,
)
details = await _read(service, 'get_message_details', context, record_id)
assert details['message']['user_id'] == expected
elif record_type == 'session':
await service.record_session_start(context, **RESOURCE, session_id='same-session', user_id=user_id)
rows, total = await _read(service, 'get_sessions', context)
assert total == 1
assert rows[0]['user_id'] == expected
else:
await service.record_feedback(context, feedback_id='same-feedback', feedback_type=1, user_id=user_id)
rows, total = await _read(service, 'get_feedback_list', context)
assert total == 1
assert rows[0]['user_id'] == expected
@pytest.mark.parametrize('user_id', [123456789, -100123456789])
async def test_query_lifecycle_persists_messages_session_and_llm_link(service, user_id, caplog):
context = _context(WORKSPACE_A)
query = _query(context, user_id)
message_id = await MonitoringHelper.record_query_start(service.ap, query, **RESOURCE)
assert message_id, caplog.text
await MonitoringHelper.record_llm_call(
service.ap,
query,
**RESOURCE,
model_name='model',
input_tokens=3,
output_tokens=5,
duration_ms=25,
message_id=message_id,
)
await MonitoringHelper.record_query_success(service.ap, message_id, query)
await MonitoringHelper.record_query_response(service.ap, query, **RESOURCE)
rows, total = await _read(service, 'get_messages', context)
assert total == 2
assert {row['role'] for row in rows} == {'user', 'assistant'}
assert {row['user_id'] for row in rows} == {str(user_id)}
details = await _read(service, 'get_message_details', context, message_id)
assert details['message']['status'] == 'success'
assert details['message']['variables'] == '{"public": "value"}'
assert details['llm_calls'][0]['message_id'] == message_id
assert details['llm_stats']['total_tokens'] == 8
sessions, total = await _read(service, 'get_sessions', context)
assert total == 1
assert sessions[0]['session_id'] == 'person_same-user'
assert sessions[0]['user_id'] == str(user_id)
assert not [record for record in caplog.records if record.levelno >= logging.ERROR]
@pytest.mark.parametrize('user_id', [123, -123])
async def test_query_error_persists_error_message_and_linked_log(service, user_id, caplog):
context = _context(WORKSPACE_A)
message_id = await MonitoringHelper.record_query_error(
service.ap,
_query(context, user_id),
**RESOURCE,
error=ValueError('failed query'),
)
assert message_id, caplog.text
details = await _read(service, 'get_message_details', context, message_id)
assert details['message']['user_id'] == str(user_id)
assert details['message']['status'] == 'error'
assert details['errors'][0]['message_id'] == message_id
assert details['errors'][0]['error_type'] == 'ValueError'
@pytest.mark.parametrize('user_id', [True, 1.5, b'123', ['123']])
@pytest.mark.parametrize('record_type', ['message', 'session', 'feedback'])
async def test_unsupported_user_ids_fail_at_the_write_boundary(service, user_id, record_type):
context = _context(WORKSPACE_A)
with pytest.raises(TypeError, match='user_id must be a string, integer, or None'):
if record_type == 'message':
await service.record_message(
context,
**RESOURCE,
message_content='hello',
session_id='session',
user_id=user_id,
)
elif record_type == 'session':
await service.record_session_start(context, **RESOURCE, session_id='session', user_id=user_id)
else:
await service.record_feedback(context, feedback_id='feedback', feedback_type=1, user_id=user_id)
async with service.ap.persistence_mgr.tenant_scope(WORKSPACE_A):
for model in (models.MonitoringMessage, models.MonitoringSession, models.MonitoringFeedback):
count = await service.ap.persistence_mgr.execute_async(sa.select(sa.func.count()).select_from(model))
assert count.scalar_one() == 0
async def test_session_analysis_aggregates_under_cloud_sql_guard(service):
context = _context(WORKSPACE_A)
await service.record_session_start(context, **RESOURCE, session_id='same-session')
await service.record_message(context, **RESOURCE, session_id='same-session', message_content='hello')
result = await _read(service, 'get_session_analysis', context, 'same-session')
assert result['found'] is True
assert result['message_stats'] == {'total': 1, 'success': 1, 'error': 0, 'pending': 0}
assert result['llm_stats']['total_calls'] == 0
assert result['tool_stats']['total_calls'] == 0
assert result['session_duration_seconds'] == 0
async def test_rls_is_enforced_without_application_workspace_predicates(service, cloud_database):
_, admin = cloud_database
for workspace in (WORKSPACE_A, WORKSPACE_B):
await service.record_message(
_context(workspace), **RESOURCE, session_id='same-session', message_content=workspace
)
async with admin.connect() as conn:
states = (
await conn.execute(
sa.text(
'SELECT relname, relrowsecurity, relforcerowsecurity FROM pg_class '
"WHERE relname LIKE 'monitoring_%' AND relkind = 'r'"
)
)
).all()
assert len(states) == len(MONITORING_TABLES)
assert all(enabled and forced for _, enabled, forced in states)
engine = service.ap.persistence_mgr.get_db_engine()
async with engine.connect() as conn:
role = (
await conn.execute(sa.text('SELECT rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user'))
).one()
assert role == (False, False)
assert (await conn.execute(sa.select(models.MonitoringMessage.id))).all() == []
for workspace in (WORKSPACE_A, WORKSPACE_B):
async with service.ap.persistence_mgr.tenant_uow(workspace):
rows = (
await service.ap.persistence_mgr.execute_async(sa.select(models.MonitoringMessage.workspace_uuid))
).all()
assert rows == [(workspace,)]
with pytest.raises(TenantScopeRequiredError):
await service.ap.persistence_mgr.execute_async(sa.select(models.MonitoringMessage.id))
async def test_traffic_series_aggregates_all_rows_under_cloud_rls(service):
import datetime
from langbot.pkg.api.http.service.monitoring_traffic import get_traffic_series
context = _context(WORKSPACE_A)
for workspace, count in ((WORKSPACE_A, 61), (WORKSPACE_B, 2)):
async with service.ap.persistence_mgr.tenant_scope(workspace):
await service.ap.persistence_mgr.execute_async(
sa.insert(models.MonitoringMessage).values(
[
dict(
workspace_uuid=workspace,
id=f'{workspace}-m-{i}',
**RESOURCE,
session_id='shared',
message_content='test',
status='success',
level='info',
timestamp=datetime.datetime(2026, 9, 11, 1, 30),
)
for i in range(count)
]
)
)
async with service.ap.persistence_mgr.tenant_uow(WORKSPACE_A):
result = await get_traffic_series(
service.ap,
context,
bot_ids=[RESOURCE['bot_id']],
start_time=datetime.datetime(2026, 9, 11),
end_time=datetime.datetime(2026, 9, 12),
)
assert result['truncated'] is False
assert sum(point['messages'] for point in result['points']) == 61
@@ -0,0 +1,226 @@
"""Disposable PostgreSQL only: real row-lock admission, rollback, and FORCE RLS."""
import asyncio
import os
import copy
import pytest
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import create_async_engine
from tests.unit_tests.api.service import test_pipeline_migration as base
from langbot.pkg.agent.runner.interaction_store import InteractionStore, InteractionScopeError
from langbot.pkg.persistence.pipeline_admission import lock_pipeline_admission
from langbot.pkg.entity.persistence.agent_interaction import AgentInteraction
URL = os.environ.get('LANGBOT_ADMISSION_TEST_URL')
pytestmark = [pytest.mark.asyncio, pytest.mark.skipif(not URL, reason='disposable PostgreSQL URL required')]
@pytest.fixture
async def env(tmp_path, monkeypatch):
admin = create_async_engine(URL)
async with admin.begin() as conn:
await conn.execute(sa.text('DROP SCHEMA public CASCADE'))
await conn.execute(sa.text('CREATE SCHEMA public'))
await conn.execute(
sa.text(
'DO $$ BEGIN CREATE ROLE admission_runtime LOGIN; EXCEPTION WHEN duplicate_object THEN NULL; END $$'
)
)
monkeypatch.setattr(base, 'create_async_engine', lambda *a, **kw: admin)
async for env in base.env.__wrapped__(tmp_path, monkeypatch):
async with admin.begin() as conn:
for table in ['legacy_pipelines', 'pipeline_migration_snapshots']:
await conn.execute(sa.text(f'ALTER TABLE {table} ENABLE ROW LEVEL SECURITY'))
await conn.execute(sa.text(f'ALTER TABLE {table} FORCE ROW LEVEL SECURITY'))
await conn.execute(
sa.text(
f"CREATE POLICY isolation ON {table} USING (workspace_uuid = current_setting('langbot.workspace_uuid', true)) WITH CHECK (workspace_uuid = current_setting('langbot.workspace_uuid', true))"
)
)
await conn.execute(sa.text('GRANT USAGE ON SCHEMA public TO admission_runtime'))
await conn.execute(
sa.text('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO admission_runtime')
)
await conn.execute(sa.text('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO admission_runtime'))
runtime = create_async_engine(URL.replace('postgres@', 'admission_runtime@'))
env.pm.db = base.NS(get_engine=lambda: runtime)
env.runtime = runtime
yield env
await runtime.dispose()
async def write(env, workspace=base.WS, pipeline='one', expected=None, check=lambda: True):
return await InteractionStore(env.runtime).create_request(
interaction_id='form',
run_id='run',
binding_id='binding',
runner_id=base.RID,
processor_type='pipeline',
processor_id=pipeline,
workspace_id=workspace,
request={},
delivery_target={},
expected_config=copy.deepcopy(base.SOURCE if expected is None else expected),
authority_check=check,
)
async def blocked(env):
# Observe PostgreSQL's lock waiter, not an arbitrary scheduling sleep.
async with asyncio.timeout(5):
while True:
async with env.engine.connect() as conn:
n = (
await conn.execute(
sa.text(
"SELECT count(*) FROM pg_stat_activity WHERE usename='admission_runtime' AND wait_event_type='Lock'"
)
)
).scalar()
if n:
return
await asyncio.sleep(0)
@pytest.mark.parametrize('outcome', ['commit', 'rollback', 'cancel'])
async def test_real_writer_waits_and_revalidates_after_transaction(env, outcome):
writer = None
try:
async with env.pm.tenant_uow(base.WS) as uow:
await lock_pipeline_admission(uow.session, base.WS, 'one')
writer = asyncio.create_task(write(env))
await blocked(env)
assert not writer.done()
async with env.engine.connect() as conn:
assert not (await conn.execute(sa.select(AgentInteraction))).all()
await uow.session.execute(
sa.update(base.LegacyPipeline)
.where(base.LegacyPipeline.uuid == 'one')
.values(config=base.planner(base.SOURCE)['config'])
)
if outcome == 'rollback':
raise RuntimeError('rollback')
if outcome == 'cancel':
raise asyncio.CancelledError()
except (RuntimeError, asyncio.CancelledError):
assert outcome != 'commit'
if outcome == 'commit':
with pytest.raises(InteractionScopeError):
await writer
else:
assert (await writer)[0]['status'] == 'pending'
async def test_real_writer_first_blocks_migration_and_other_tenant_isolated(env):
await write(env)
async with env.pm.tenant_uow(base.WS) as uow:
await lock_pipeline_admission(uow.session, base.WS, 'one')
assert (
await uow.session.execute(
sa.select(AgentInteraction.status).where(AgentInteraction.workspace_id == base.WS)
)
).scalar_one() == 'pending'
with pytest.raises(InteractionScopeError):
await write(env, workspace=base.OTHER)
async with env.runtime.connect() as conn:
assert not (await conn.execute(sa.select(base.LegacyPipeline))).all()
async with env.pm.tenant_uow(base.OTHER) as uow:
assert (await uow.session.execute(sa.select(base.LegacyPipeline.uuid))).scalars().all() == ['foreign']
async def test_real_migration_commit_activation_and_stale_writer(env):
original = env.svc._activate
async def activate(*args):
with pytest.raises(InteractionScopeError):
await write(env)
return await original(*args)
env.svc._activate = activate
task = await base.execute(env)
assert task.task_context.metadata['results'][0]['state'] == 'migrated'
with pytest.raises(InteractionScopeError):
await write(env)
_, snapshots = await base.rows(env)
assert snapshots[0]['state'] == 'active'
@pytest.mark.parametrize('boundary', ['cas', 'activation'])
async def test_real_no_phantom_between_check_and_publication(env, monkeypatch, boundary):
writer = None
sql = env.pm.execute_async
state = env.svc._interaction_state
entered_activation = False
activate = env.svc._activate
async def activation(*args):
nonlocal entered_activation
entered_activation = True
return await activate(*args)
async def start_waiter():
nonlocal writer
writer = asyncio.create_task(write(env))
await blocked(env)
assert not writer.done()
async def execute_sql(statement, *args, **kwargs):
result = await sql(statement, *args, **kwargs)
if (
boundary == 'cas'
and isinstance(statement, sa.sql.dml.Insert)
and statement.table.name == 'pipeline_migration_snapshots'
):
await start_waiter()
return result
async def interaction_state(*args):
result = await state(*args)
if boundary == 'activation' and entered_activation:
await start_waiter()
return result
monkeypatch.setattr(env.pm, 'execute_async', execute_sql)
monkeypatch.setattr(env.svc, '_activate', activation)
monkeypatch.setattr(env.svc, '_interaction_state', interaction_state)
task = await base.execute(env)
assert task.task_context.metadata['results'][0]['state'] == 'migrated'
with pytest.raises(InteractionScopeError):
await writer
async with env.engine.connect() as conn:
assert not (await conn.execute(sa.select(AgentInteraction))).all()
async def test_real_conversation_revoked_while_waiting(env):
live = True
async with env.pm.tenant_uow(base.WS) as uow:
await lock_pipeline_admission(uow.session, base.WS, 'one')
writer = asyncio.create_task(write(env, check=lambda: live))
await blocked(env)
live = False
with pytest.raises(InteractionScopeError):
await writer
async def test_real_other_tenant_writer_is_not_serialized(env):
async with env.pm.tenant_uow(base.WS) as uow:
await lock_pipeline_admission(uow.session, base.WS, 'one')
record, _ = await asyncio.wait_for(write(env, workspace=base.OTHER, pipeline='foreign'), 3)
assert record['workspace_id'] == base.OTHER
async def test_real_pending_before_commit_prevents_migration(env):
body = await base.selection(env)
await write(env)
with pytest.raises(env.m.MigrationError, match='preview_stale'):
await base.execute(env, body)
configs, snapshots = await base.rows(env)
assert configs['one'] == base.SOURCE and not snapshots
env.ap.pipeline_mgr.publish_pipeline.assert_not_called()
# Run the unchanged cancellation/ambiguous-acknowledgement contracts on real PG.
test_real_cancel_commit_outcome = base.test_cancel_commit_reports_durable_outcome_and_stops_batch
test_real_cancel_activation_retry = base.test_cancel_activation_retry_reconciles_original_snapshot
test_real_unavailable_reconciliation = base.test_unavailable_commit_reconciliation_is_conservative
test_real_cancel_prepare = base.test_cancel_during_prepare_keeps_original_and_stops_batch
@@ -142,7 +142,7 @@ async def test_legacy_sqlite_resources_are_backfilled_and_contracted(tmp_path):
assert pk_columns == {
'binary_storages': ('workspace_uuid', 'unique_key'),
'plugin_settings': ('workspace_uuid', 'plugin_author', 'plugin_name'),
'monitoring_sessions': ('workspace_uuid', 'session_id'),
'monitoring_sessions': ('workspace_uuid', 'bot_id', 'session_id'),
}
pipeline_run_foreign_keys = await _inspect(
@@ -237,8 +237,10 @@ async def test_sqlite_scoped_keys_allow_cross_workspace_but_reject_same_workspac
await conn.execute(
sa.text(
'INSERT INTO monitoring_sessions '
'(workspace_uuid, session_id, bot_id, last_activity, is_active) '
"VALUES (:workspace_uuid, 'session-1', 'bot-2', CURRENT_TIMESTAMP, 1)"
'(workspace_uuid, session_id, bot_id, bot_name, pipeline_id, pipeline_name, '
'start_time, last_activity, message_count, is_active) '
"VALUES (:workspace_uuid, 'session-1', 'bot-2', 'bot', 'pipeline-2', 'pipeline', "
'CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 1)'
),
{'workspace_uuid': second_workspace_uuid},
)
@@ -5,6 +5,7 @@ import logging
import os
import pathlib
import sqlite3
from contextlib import closing
import pytest
import sqlalchemy as sa
@@ -34,7 +35,7 @@ def _manifest_payloads(backup_directory) -> list[dict]:
def _assert_verified_backup(payload: dict) -> None:
backup_path = pathlib.Path(payload['backup_path'])
with sqlite3.connect(f'{backup_path.as_uri()}?mode=ro', uri=True) as connection:
with closing(sqlite3.connect(f'{backup_path.as_uri()}?mode=ro', uri=True)) as connection:
assert connection.execute('PRAGMA quick_check').fetchall() == [('ok',)]
assert connection.execute('SELECT version_num FROM alembic_version').fetchone()[0] == payload['source_revision']
@@ -444,10 +444,12 @@ async def test_persistence_startup_defers_workspace_tables_until_account_upgrade
await conn.run_sync(lambda sync_conn: sa.inspect(sync_conn).get_table_names())
)
assert 'workspaces' not in tables_before_migration
assert 'codex_credentials' not in tables_before_migration
await manager._run_alembic_migrations()
async with engine.connect() as conn:
assert 'codex_credentials' in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
workspace = (
(await conn.execute(sa.text("SELECT * FROM workspaces WHERE source = 'local'"))).mappings().one()
)
@@ -0,0 +1,309 @@
"""Real Core/SDK protocol regression tests; no subprocesses or external services.
Run against the intended local SDK (``uv run --no-sync`` after local install).
The in-memory transport carries JSON strings through Handler.run on both sides;
send_file, envelope validation, base64 decoding and transfer storage are real.
Only Core's database/object-storage services, parser dispatch/provider and host
sandbox prerequisite probing are doubles. Worker launch/registration is
represented by its already-registered state.
"""
from __future__ import annotations
import asyncio
import json
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from langbot.pkg.plugin.handler import RuntimeConnectionHandler
from langbot_plugin.entities.io.actions.enums import CommonAction, LangBotToRuntimeAction, PluginToRuntimeAction
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding, PluginWorkerPolicy, RuntimeIdentity
from langbot_plugin.runtime.context import RuntimeContext
from langbot_plugin.runtime.io.connection import Connection
from langbot_plugin.entities.io.errors import ActionCallError, ConnectionClosedError
from langbot_plugin.runtime.io.handler import FILE_CHUNK_LENGTH, Handler
from langbot_plugin.runtime.io.handlers.control import ControlConnectionHandler
from langbot_plugin.runtime.io.handlers.plugin import PluginConnectionHandler
from langbot_plugin.runtime.plugin.mgr import PluginManager
from langbot_plugin.runtime.security import PLUGIN_FILE_STORAGE_DIR_ENV
pytestmark = pytest.mark.asyncio
PAYLOAD = bytes(range(256)) * 161 + b'\x00original RAG file\xff'
BINDING = InstallationBinding(
instance_uuid='instance-a',
workspace_uuid='workspace-a',
placement_generation=7,
installation_uuid='00000000-0000-4000-8000-000000000001',
runtime_revision=3,
artifact_digest='a' * 64,
)
LEGACY = ActionContext(**BINDING.model_dump(exclude={'runtime_revision', 'artifact_digest'}))
class QueueConnection(Connection):
"""Only the byte transport is replaced, not the request/response machinery."""
def __init__(self):
self.incoming = asyncio.Queue()
self.sent = []
self.peer = None
async def send(self, message: str) -> None:
assert isinstance(message, str)
self.sent.append(json.loads(message))
await self.peer.incoming.put(message)
async def receive(self) -> str:
message = await self.incoming.get()
if message is None:
raise ConnectionClosedError('test transport closed')
return message
async def close(self) -> None:
await self.incoming.put(None)
await self.peer.incoming.put(None)
def connection_pair():
left, right = QueueConnection(), QueueConnection()
left.peer, right.peer = right, left
return left, right
@asynccontextmanager
async def protocol_stack(tmp_path, monkeypatch, profile='oss_dev', binding=LEGACY):
monkeypatch.chdir(tmp_path)
stored = tmp_path / 'original.bin'
stored.write_bytes(PAYLOAD)
storage_calls = []
async def get_file_stream(execution_context, storage_path):
storage_calls.append((execution_context, storage_path))
assert execution_context.workspace_uuid == BINDING.workspace_uuid
assert storage_path == 'knowledge/original.bin'
return stored.read_bytes()
async def get_execution_binding(workspace_uuid, expected_generation):
assert workspace_uuid == BINDING.workspace_uuid
assert expected_generation == BINDING.placement_generation
return BINDING
setting = SimpleNamespace(
plugin_author='tester',
plugin_name='engine',
installation_uuid=BINDING.installation_uuid,
runtime_revision=BINDING.runtime_revision,
artifact_digest=BINDING.artifact_digest,
)
app = SimpleNamespace(
deployment=SimpleNamespace(mode='oss' if profile == 'oss_dev' else 'cloud'),
logger=logging.getLogger(__name__),
persistence_mgr=SimpleNamespace(execute_async=AsyncMock(return_value=SimpleNamespace(first=lambda: setting))),
workspace_service=SimpleNamespace(get_execution_binding=get_execution_binding),
rag_runtime_service=SimpleNamespace(get_file_stream=get_file_stream),
)
core_conn, control_conn = connection_pair()
monkeypatch.setenv(PLUGIN_FILE_STORAGE_DIR_ENV, str(tmp_path / 'core-transfer'))
core = RuntimeConnectionHandler(core_conn, AsyncMock(return_value=False), app)
core.register_installation_binding(BINDING, plugin_author='tester', plugin_name='engine')
runtime = RuntimeContext()
runtime.plugin_mgr = PluginManager(runtime)
# No worker is launched: omit only host nsjail/cgroup prerequisite probing.
monkeypatch.setattr(runtime.plugin_mgr.worker_launcher, 'configure', lambda policy, profile: None)
monkeypatch.setenv(PLUGIN_FILE_STORAGE_DIR_ENV, str(tmp_path / 'runtime-transfer'))
control = ControlConnectionHandler(control_conn, runtime)
runtime.activate_control_handler(control)
bridge_conn, plugin_conn = connection_pair()
bridge = PluginConnectionHandler(bridge_conn, runtime, file_storage_dir=str(tmp_path / 'bridge-transfer'))
plugin = Handler(plugin_conn, file_storage_dir=str(tmp_path / 'plugin-transfer'))
# Trusted state left by registration, not plugin-supplied action data.
bridge.bind_action_context(binding)
runtime.plugin_mgr.plugin_handlers.append(bridge)
runtime.plugin_mgr.plugins.append(SimpleNamespace(_runtime_plugin_handler=bridge))
handlers = [core, control, bridge, plugin]
tasks = [asyncio.create_task(handler.run()) for handler in handlers]
try:
await asyncio.wait_for(
core.set_runtime_config(
runtime_identity=RuntimeIdentity(instance_uuid='instance-a', runtime_id='test-runtime'),
worker_policy=PluginWorkerPolicy(
max_cpus=1,
max_memory_mb=128,
max_pids=32,
max_open_files=64,
max_file_size_mb=8,
require_hard_limits=False,
),
runtime_profile=profile,
cloud_service_url=None,
),
5,
)
if isinstance(binding, InstallationBinding):
runtime.activate_installation_binding(binding)
else:
runtime.bind_workspace(binding)
yield SimpleNamespace(
core=core,
control=control,
runtime=runtime,
bridge=bridge,
plugin=plugin,
core_conn=core_conn,
control_conn=control_conn,
bridge_conn=bridge_conn,
plugin_conn=plugin_conn,
app=app,
storage_calls=storage_calls,
)
finally:
for handler in handlers:
await handler.close()
await asyncio.wait_for(asyncio.gather(*tasks, return_exceptions=True), 5)
def assert_chunks(connection, binding, payload=PAYLOAD):
chunks = [message for message in connection.sent if message.get('action') == CommonAction.FILE_CHUNK.value]
expected = (len(payload) + FILE_CHUNK_LENGTH - 1) // FILE_CHUNK_LENGTH
assert expected > 1
assert len(chunks) == expected
assert [chunk['data']['chunk_index'] for chunk in chunks] == list(range(expected))
assert {chunk['data']['chunk_amount'] for chunk in chunks} == {expected}
assert all(chunk['context'] == binding.model_dump() for chunk in chunks)
assert len({chunk['data']['file_key'] for chunk in chunks}) == 1
return chunks[0]['data']['file_key']
@pytest.mark.parametrize(
'profile,binding',
[('oss_dev', LEGACY), ('oss_dev', BINDING), ('shared', BINDING)],
ids=['legacy-oss', 'managed-oss', 'managed-shared'],
)
async def test_knowledge_file_roundtrip_reaches_plugin_original_bytes(tmp_path, monkeypatch, profile, binding):
async with protocol_stack(tmp_path, monkeypatch, profile, binding) as stack:
# Legacy plugin API sends no authority; Runtime supplies its trusted binding.
result = await asyncio.wait_for(
stack.plugin.call_action(
PluginToRuntimeAction.GET_KNOWLEDEGE_FILE_STREAM,
{'storage_path': 'knowledge/original.bin'},
),
5,
)
assert await stack.plugin.read_local_file(result['file_key']) == PAYLOAD
assert len(stack.storage_calls) == 1
# Core resolves persisted installation authority even for legacy callers;
# the Runtime still preserves the legacy envelope on the plugin hop.
core_key = assert_chunks(stack.core_conn, BINDING)
plugin_key = assert_chunks(stack.bridge_conn, binding)
assert result['file_key'] == plugin_key != core_key
assert not (Path(stack.control.file_storage_dir) / core_key).exists()
assert not stack.control._owned_transfer_files
callbacks = [
message
for message in stack.control_conn.sent
if message.get('action') == PluginToRuntimeAction.GET_KNOWLEDEGE_FILE_STREAM.value
]
assert len(callbacks) == 1
assert callbacks[0]['context'] == binding.model_dump()
assert callbacks[0]['data'] == {'storage_path': 'knowledge/original.bin'}
async def test_shared_control_rejects_legacy_chunks_before_storage(tmp_path, monkeypatch):
async with protocol_stack(tmp_path, monkeypatch, 'shared', BINDING) as stack:
with stack.core.installation_scope(LEGACY):
with pytest.raises(ActionCallError, match='InstallationBinding|Legacy FILE_CHUNK'):
await asyncio.wait_for(stack.core.send_file(PAYLOAD, ''), 5)
assert not list(Path(stack.control.file_storage_dir).iterdir())
assert not stack.control._owned_transfer_files
async def test_candidate_artifact_pretransfer_does_not_require_active_installation(tmp_path, monkeypatch):
async with protocol_stack(tmp_path, monkeypatch, 'shared', BINDING) as stack:
candidate = BINDING.model_copy(
update={'installation_uuid': 'candidate-installation', 'runtime_revision': 1, 'artifact_digest': 'c' * 64}
)
assert not stack.runtime.is_current_installation_binding(candidate)
with stack.core.installation_scope(candidate):
key = await asyncio.wait_for(stack.core.send_file(PAYLOAD, 'lbp'), 5)
assert_chunks(stack.core_conn, candidate)
assert await stack.control.read_local_file(key) == PAYLOAD
assert not stack.runtime.is_current_installation_binding(candidate)
async def test_nested_parser_target_owns_file_and_action_envelopes(tmp_path, monkeypatch):
async with protocol_stack(tmp_path, monkeypatch, 'shared', BINDING) as stack:
target = BINDING.model_copy(
update={
'installation_uuid': 'parser-installation',
'runtime_revision': 2,
'artifact_digest': 'b' * 64,
}
)
stack.runtime.activate_installation_binding(target)
parser_calls = []
restored = []
async def parse_document(author, name, context_data, file_bytes):
parser_calls.append((stack.control.current_action_context, author, name, context_data, file_bytes))
return {'documents': [{'text': 'parsed'}]}
stack.runtime.plugin_mgr.parse_document = parse_document
class ParserConnector:
async def require_workspace_context(self, context):
assert context.workspace_uuid == BINDING.workspace_uuid
async def call_parser(self, plugin_name, context_data, file_bytes):
assert plugin_name == 'tester/parser'
assert stack.core.current_action_context == BINDING
with stack.core.installation_scope(target):
result = await stack.core.parse_document('tester', 'parser', context_data, file_bytes)
restored.append(stack.core.resolve_outbound_action_context(None))
return result
stack.app.plugin_connector = ParserConnector()
result = await asyncio.wait_for(
stack.plugin.call_action(
PluginToRuntimeAction.INVOKE_PARSER,
{
'plugin_author': 'tester',
'plugin_name': 'parser',
'storage_path': 'knowledge/original.bin',
'filename': 'original.bin',
},
),
5,
)
assert result == {'documents': [{'text': 'parsed'}]}
key = assert_chunks(stack.core_conn, target)
parse_requests = [
message
for message in stack.core_conn.sent
if message.get('action') == LangBotToRuntimeAction.PARSE_DOCUMENT.value
]
assert len(parse_requests) == 1
assert parse_requests[0]['context'] == target.model_dump()
assert parse_requests[0]['data']['context']['file_key'] == key
assert parser_calls == [
(
target,
'tester',
'parser',
{
'mime_type': 'application/octet-stream',
'filename': 'original.bin',
'metadata': {},
},
PAYLOAD,
)
]
assert restored == [BINDING]
assert stack.core.current_action_context is None
assert stack.core.resolve_outbound_action_context(None) is None
assert not (Path(stack.control.file_storage_dir) / key).exists()
+123
View File
@@ -0,0 +1,123 @@
"""Real embedded SeekDB regression tests.
Install the optional dependency before running these slow tests::
uv sync --dev --extra seekdb
uv run pytest tests/integration/vector/test_seekdb.py -m slow -q
"""
from __future__ import annotations
import asyncio
from types import SimpleNamespace
import uuid
import pytest
pytest.importorskip('pyseekdb')
from langbot.pkg.vector.vdbs.seekdb import SeekDBVectorDatabase
pytestmark = [pytest.mark.integration, pytest.mark.slow]
@pytest.fixture
async def backend(tmp_path):
app = SimpleNamespace(
instance_config=SimpleNamespace(
data={
'vdb': {
'runtime_cache_limit': 16,
'seekdb': {
'mode': 'embedded',
'path': str(tmp_path),
'database': 'langbot_test',
},
}
}
),
logger=SimpleNamespace(
info=lambda *args, **kwargs: None,
warning=lambda *args, **kwargs: None,
),
)
database = SeekDBVectorDatabase(app)
collection = f'test_{uuid.uuid4().hex}'
yield database, collection
await database.delete_collection(collection)
await database.close()
@pytest.mark.asyncio
async def test_upsert_and_text_round_trip(backend) -> None:
database, collection = backend
original = 'He said "hello".\nC:\\notes\\file.txt isn\'t empty. 中文'
updated = f'Updated: {original}'
await database.add_embeddings(
collection,
['document-a'],
[[1.0, 0.0, 0.0]],
[{'file_id': 'file-a', 'text': original}],
[original],
)
await database.add_embeddings(
collection,
['document-a'],
[[0.0, 1.0, 0.0]],
[{'file_id': 'file-a', 'text': updated}],
[updated],
)
items, _ = await database.list_by_filter(collection, {'file_id': 'file-a'})
assert len(items) == 1
assert items[0]['id'] == 'document-a'
assert items[0]['document'] == updated
assert items[0]['metadata']['text'] == updated
@pytest.mark.asyncio
async def test_full_text_and_hybrid_results_keep_relevance_order(backend) -> None:
database, collection = backend
documents = [
'orchid orchid orchid flower',
'orchid grows in a garden with many other beautiful plants',
'a completely unrelated topic',
]
await database.add_embeddings(
collection,
['best', 'weak', 'noise'],
[[1.0, 0.0, 0.0], [0.9, 0.1, 0.0], [0.0, 0.0, 1.0]],
[
{'file_id': item_id, 'document_id': item_id, 'text': document}
for item_id, document in zip(['best', 'weak', 'noise'], documents, strict=True)
],
documents,
)
seekdb_collection = await database.get_or_create_collection(collection)
await asyncio.to_thread(seekdb_collection.refresh_index)
full_text = await database.search(
collection,
[1.0, 0.0, 0.0],
k=3,
search_type='full_text',
query_text='orchid',
)
hybrid = await database.search(
collection,
[1.0, 0.0, 0.0],
k=3,
search_type='hybrid',
query_text='orchid',
vector_weight=0.65,
)
assert full_text['ids'][0][:2] == ['best', 'weak']
assert full_text['distances'][0] == sorted(full_text['distances'][0])
assert hybrid['ids'][0] == ['best', 'weak', 'noise']
assert hybrid['distances'][0] == sorted(hybrid['distances'][0])