mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
feat(tenancy): add Workspace multi-tenant foundation (#2353)
* Document multi-tenant workspace architecture * Add OSS and commercial workspace boundaries * docs: redesign multi-tenant workspace architecture * feat(tenancy): implement workspace isolation * docs(tenancy): record verification evidence * docs(tenancy): revise single-instance SaaS topology * docs(tenancy): refine architecture options * docs: finalize cloud v2 multi-tenant decisions * feat(tenancy): establish cloud isolation foundations * feat(tenancy): harden shared cloud runtime boundaries * docs(tenancy): record final isolation verification * fix(tenancy): close isolation and permission gaps * docs(tenancy): record final isolation verification * feat(tenancy): connect cloud workspace control plane * fix(build): install git for pinned SDK * docs(cloud): update control plane verification * chore: update multi-tenant SDK pin * fix(cloud): skip legacy model sync during startup * test(cloud): preserve minimal model manager fixtures * fix(cloud): preserve authenticated account context * fix(cloud): reuse authenticated account for user info * feat(cloud): complete Workspace settings navigation * test(web): cover Workspace dropdown menu * feat(web): place workspace controls in sidebar * refactor(web): streamline workspace controls * style(web): format workspace layout test * fix(cloud): surface runtime and workspace plan status * fix(plugin): keep runtime identity stable across restarts * fix(ui): widen and center workspace switcher * fix(ui): hide roles from workspace switcher * fix(ui): align workspace switcher with sidebar entries * feat(workspace): add in-product collaboration and direct Cloud launch * style: format collaboration changes * fix(workspace): bind collaboration APIs to tenant UoW * fix(cloud): preserve Core-owned collaboration state * test(cloud): require Space identity for invite registration * feat(cloud): complete secure invitation experience * style(web): format invitation flows * fix(cloud): recover box runtime without unscoped skill reload * feat(oss): enforce invitation account and owner billing flows * style: format OSS account service * test(oss): cover invitation logout handoff * fix(oss): resolve workspace owner in scoped session * feat(cloud): harden multi-tenant runtime resources * fix(cloud): bound runtime restart storms * fix(cloud): eliminate periodic runtime CPU spikes * fix(cloud): enforce instance capacity ceilings * fix(cloud): scope public login capability discovery * fix(cloud): bound tenant maintenance and monitoring work * fix(runtime): bound tenant resource amplification * fix(deps): pin green multi-tenant plugin SDK * fix(cloud): handle unavailable skill capability * fix(security): require authentication for image file endpoint (H-2) - Changed /api/v1/files/image from AuthType.NONE to USER_TOKEN_OR_API_KEY - Added Permission.RESOURCE_VIEW requirement - Prevents unauthenticated cross-tenant file access via leaked keys - Fixes HIGH severity finding from multi-tenant security review docs: add comprehensive database migration guide - Complete migration steps for OSS → multi-tenant - Backup, execution, verification procedures - Rollback scenarios and recovery plans - Performance tuning recommendations * test: add comprehensive cross-tenant isolation tests Added 7 critical test scenarios for multi-tenant boundaries: - Cross-tenant bot access prevention - Viewer role read-only enforcement - Removed member immediate access revocation - Model provider credential isolation - WebSocket message isolation - Invitation token workspace scoping - Multi-workspace context validation These tests address P0-2 coverage gaps for: - workspaces.py (membership & invitation flows) - user.py (authentication & authorization) - websocket_chat.py (real-time isolation) - plugins.py (resource access control) docs: finalize database migration guide * fix(security): resolve M-1, M-2, M-3 security findings M-1: WebSocket authorization TOCTOU race (FIXED) - Changed _revalidate_websocket_authorization to return RequestContext - Ensures validated context is used immediately without race window - Prevents removed members from sending messages during revalidation gap M-2: Model Manager cache workspace isolation (VERIFIED) - Confirmed _CacheKey already uses 4-tuple: (instance, workspace, generation, resource) - Cache is properly scoped per workspace, no cross-tenant leakage possible - No code change needed, documented as working correctly M-3: Invitation lock workspace scoping (FIXED) - Changed lock key from token_digest to workspace_uuid:token_digest - Prevents DoS where attacker locks token in Workspace A to block Workspace B - Locks now isolated per workspace All MEDIUM severity findings from security review now resolved. * fix(cloud): unblock tenant CI and enforce knowledge quotas * fix(tenancy): scope rerank model sync --------- Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
This commit is contained in:
@@ -2,12 +2,42 @@ api:
|
||||
port: 5300
|
||||
webhook_prefix: 'http://127.0.0.1:5300'
|
||||
extra_webhook_prefix: ''
|
||||
# Canonical browser origin when WebUI and API use different origins in
|
||||
# development (for example http://localhost:3000). Production bundled UI
|
||||
# may leave this empty when webhook_prefix already has the browser origin.
|
||||
# OAuth redirects trust only these server-side values, never request Host
|
||||
# or Origin headers.
|
||||
webui_url: ''
|
||||
# Global API key for the HTTP service API and the MCP server. When set to a
|
||||
# non-empty string, this key is accepted anywhere a web-UI-created API key is
|
||||
# accepted (X-API-Key header or "Authorization: Bearer <key>"), WITHOUT any
|
||||
# login session and without a database record. Leave empty to disable.
|
||||
# Keep this value secret; only enable it on trusted/internal deployments.
|
||||
global_api_key: ''
|
||||
workspace:
|
||||
invitations:
|
||||
# Public WebUI origin used to build invitation links. Leave empty to
|
||||
# use api.webui_url, then api.webhook_prefix. Set via
|
||||
# WORKSPACE__INVITATIONS__PUBLIC_WEB_URL in container deployments.
|
||||
public_web_url: ''
|
||||
email:
|
||||
# Optional invitation email delivery. Empty provider keeps
|
||||
# invitations link-only. Supported: resend, smtp.
|
||||
provider: ''
|
||||
from: ''
|
||||
timeout_seconds: 10
|
||||
resend:
|
||||
api_url: 'https://api.resend.com/emails'
|
||||
# Secret. Set via WORKSPACE__INVITATIONS__EMAIL__RESEND__API_KEY.
|
||||
api_key: ''
|
||||
smtp:
|
||||
host: ''
|
||||
port: 587
|
||||
username: ''
|
||||
# Secret. Set via WORKSPACE__INVITATIONS__EMAIL__SMTP__PASSWORD.
|
||||
password: ''
|
||||
starttls: true
|
||||
ssl: false
|
||||
command:
|
||||
enable: true
|
||||
prefix:
|
||||
@@ -17,6 +47,35 @@ command:
|
||||
concurrency:
|
||||
pipeline: 20
|
||||
session: 1
|
||||
# Hard admission limits for queued + running pipeline queries.
|
||||
pending_queries: 1000
|
||||
pending_queries_per_workspace: 100
|
||||
webhooks:
|
||||
# Bound database materialization and per-message outbound fan-out.
|
||||
# Existing rows above this limit remain deletable through the management
|
||||
# API, but only this many enabled destinations are dispatched.
|
||||
# Supports WEBHOOKS__MAX_PER_WORKSPACE (hard cap: 64).
|
||||
max_per_workspace: 16
|
||||
# Instance-wide request admission. Delivery fails open when every slot is
|
||||
# occupied instead of retaining an unbounded queue of webhook tasks.
|
||||
# Supports WEBHOOKS__MAX_INFLIGHT_REQUESTS (hard cap: 128).
|
||||
max_inflight_requests: 16
|
||||
cloud:
|
||||
# Operational safety ceilings for the one logical Cloud instance. These
|
||||
# are not subscription entitlements. An authoritative directory update
|
||||
# that would exceed them is rejected atomically rather than truncated.
|
||||
directory:
|
||||
# Tune downward from the measured production capacity curve. Core has
|
||||
# an absolute safety ceiling of 5,000 active Workspaces.
|
||||
max_active_workspaces: 1000
|
||||
# Full snapshots contain current Workspaces only. Archived tombstones
|
||||
# are delivered through bounded per-Workspace deltas.
|
||||
max_snapshot_workspaces: 1000
|
||||
# Aggregate memberships accepted in one signed snapshot or delta.
|
||||
max_snapshot_memberships: 20000
|
||||
# Signed control-plane envelope buffered by the closed adapter before
|
||||
# JSON/JWS verification (32 MiB; absolute maximum 64 MiB).
|
||||
max_response_bytes: 33554432
|
||||
proxy:
|
||||
http: ''
|
||||
https: ''
|
||||
@@ -26,6 +85,15 @@ system:
|
||||
recovery_key: ''
|
||||
allow_modify_login_info: true
|
||||
disabled_adapters: []
|
||||
blocking_executor:
|
||||
# All asyncio.to_thread work shares this process-wide bounded pool.
|
||||
# Both running threads and queued calls are capped to prevent tenant
|
||||
# bursts from creating an unbounded queue of retained request objects.
|
||||
max_workers: 8
|
||||
max_pending: 128
|
||||
# One trusted Workspace can occupy at most this many running + queued
|
||||
# slots. This must not exceed half of max_workers.
|
||||
max_inflight_per_scope: 4
|
||||
# Public outbound IP addresses of this LangBot deployment. Some platforms
|
||||
# (e.g. WeCom, WeChat Official Account, QQ Official API) require the
|
||||
# caller's IPs to be added to their trusted-IP / IP-whitelist settings.
|
||||
@@ -37,6 +105,7 @@ system:
|
||||
max_bots: -1
|
||||
max_pipelines: -1
|
||||
max_extensions: -1
|
||||
max_knowledge_bases: -1
|
||||
# When set to a non-empty string, every pipeline is forced to use this
|
||||
# Box sandbox-scope template regardless of its own configuration, and
|
||||
# the per-pipeline "Sandbox Scope" selector is locked in the web UI.
|
||||
@@ -46,6 +115,32 @@ system:
|
||||
task_retention:
|
||||
# Keep at most this many completed async task records in memory
|
||||
completed_limit: 200
|
||||
# Bound progress output retained by one task, including running tasks.
|
||||
max_log_chars: 200000
|
||||
# Protect the shared process from user-triggered operation storms.
|
||||
max_active_user_tasks: 256
|
||||
max_active_user_tasks_per_workspace: 8
|
||||
session_retention:
|
||||
# Process-local conversation sessions are a cache, not durable history.
|
||||
max_entries: 2000
|
||||
max_entries_per_workspace: 200
|
||||
idle_ttl_seconds: 86400
|
||||
max_conversations_per_session: 20
|
||||
max_messages_per_conversation: 100
|
||||
websocket_retention:
|
||||
# Bound live browser sockets and per-Workspace fan-out in the shared process.
|
||||
max_connections: 1024
|
||||
max_connections_per_workspace: 32
|
||||
# Idle proxy runtimes are evicted when this process-local cache fills.
|
||||
max_workspace_proxies: 1024
|
||||
max_conversations_per_workspace: 200
|
||||
max_messages_per_conversation: 100
|
||||
conversation_idle_ttl_seconds: 86400
|
||||
send_queue_size: 100
|
||||
response_limits:
|
||||
# Defense in depth for tenant-configured upstream providers.
|
||||
max_generated_chars: 1048576
|
||||
max_stream_chunks: 100000
|
||||
jwt:
|
||||
expire: 604800
|
||||
secret: ''
|
||||
@@ -54,13 +149,33 @@ database:
|
||||
sqlite:
|
||||
path: 'data/langbot.db'
|
||||
postgresql:
|
||||
# Optional SQLAlchemy URL (postgresql[+asyncpg]://...). When set, it
|
||||
# overrides the structured fields and preserves TLS/query options.
|
||||
url: ''
|
||||
host: '127.0.0.1'
|
||||
port: 5432
|
||||
user: 'postgres'
|
||||
password: 'postgres'
|
||||
database: 'postgres'
|
||||
# One bounded pool is shared by business data and Cloud pgvector.
|
||||
pool_size: 10
|
||||
max_overflow: 10
|
||||
pool_timeout_seconds: 30
|
||||
pool_recycle_seconds: 1800
|
||||
# Applied only to Cloud runtime connections. The one-shot release
|
||||
# migration uses its operator connection without these short limits.
|
||||
statement_timeout_ms: 60000
|
||||
lock_timeout_ms: 5000
|
||||
idle_in_transaction_session_timeout_ms: 60000
|
||||
cloud_migration:
|
||||
# `langbot migrate --cloud` reads an operator-only PostgreSQL DSN from
|
||||
# this environment variable. The operator role must differ from the
|
||||
# runtime role above; never put its password in this file or CLI args.
|
||||
operator_dsn_env: 'LANGBOT_CLOUD_MIGRATION_DSN'
|
||||
vdb:
|
||||
use: chroma
|
||||
# Bound process-local collection/index handles across all Workspaces.
|
||||
runtime_cache_limit: 1024
|
||||
qdrant:
|
||||
url: ''
|
||||
host: localhost
|
||||
@@ -82,6 +197,11 @@ vdb:
|
||||
token: ''
|
||||
db_name: ''
|
||||
pgvector:
|
||||
# SaaS/shared-schema deployments reuse database.postgresql. OSS can
|
||||
# keep this false when deliberately using an external pgvector DB.
|
||||
use_business_database: false
|
||||
# Release migrations create one partial ANN index per enabled value.
|
||||
allowed_dimensions: [384, 512, 768, 1024, 1536]
|
||||
host: '127.0.0.1'
|
||||
port: 5433
|
||||
database: 'langbot'
|
||||
@@ -99,6 +219,9 @@ vdb:
|
||||
request_timeout: 5000 # per-request timeout in ms (glide default 250ms is too low for KNN)
|
||||
storage:
|
||||
use: local
|
||||
# Bound every object materialized into Core memory. Built-in Local/S3
|
||||
# providers enforce this while reading (hard cap: 64 MiB).
|
||||
max_object_read_bytes: 10485760
|
||||
cleanup:
|
||||
# Enable periodic cleanup of local/S3 uploaded files and old log files
|
||||
enabled: true
|
||||
@@ -108,21 +231,79 @@ storage:
|
||||
uploaded_file_retention_days: 7
|
||||
# LangBot log files older than this many days will be deleted
|
||||
log_retention_days: 3
|
||||
# Bound per-Workspace file cleanup and diagnostic candidate lists.
|
||||
# Supports STORAGE__CLEANUP__MAX_FILES_PER_RUN (hard cap: 10000).
|
||||
max_files_per_run: 1000
|
||||
s3:
|
||||
endpoint_url: ''
|
||||
access_key_id: ''
|
||||
secret_access_key: ''
|
||||
region: 'us-east-1'
|
||||
bucket: 'langbot-storage'
|
||||
# boto3 is synchronous; bound the number of operations delegated to
|
||||
# worker threads so an S3 slowdown cannot saturate the process.
|
||||
max_concurrency: 16
|
||||
plugin:
|
||||
enable: true
|
||||
runtime_ws_url: 'ws://langbot_plugin_runtime:5400/control/ws'
|
||||
enable_marketplace: true
|
||||
display_plugin_debug_url: 'ws://localhost:5401/plugin/debug/ws'
|
||||
worker:
|
||||
# Instance-wide maximum for every plugin installation. Plugin
|
||||
# manifests cannot raise or override these limits.
|
||||
max_cpus: 1.0
|
||||
max_memory_mb: 512
|
||||
max_pids: 128
|
||||
max_open_files: 256
|
||||
max_file_size_mb: 512
|
||||
# Instance-wide admission budgets. The effective worker count is the
|
||||
# lowest of max_workers, max_total_cpus/max_cpus and
|
||||
# max_total_memory_mb/max_memory_mb.
|
||||
max_workers: 16
|
||||
max_total_cpus: 8.0
|
||||
max_total_memory_mb: 8192
|
||||
# Includes disabled and historical installation fences retained to
|
||||
# reject stale desired-state replay.
|
||||
max_installations: 10000
|
||||
# Restart storms are globally serialized by default. Repeated
|
||||
# unexpected exits within the configured window open a Runtime-wide
|
||||
# circuit; one half-open probe must remain stable before other
|
||||
# installations may restart.
|
||||
max_concurrent_restarts: 1
|
||||
restart_failure_threshold: 8
|
||||
restart_failure_window_seconds: 30.0
|
||||
restart_circuit_open_seconds: 60.0
|
||||
# Cloud shared Runtime sets this to true and fails closed unless
|
||||
# delegated cgroup v2 controllers are available.
|
||||
require_hard_limits: false
|
||||
binary_storage:
|
||||
# Max bytes for a single plugin binary storage value
|
||||
max_value_bytes: 10485760
|
||||
mcp:
|
||||
# Bound instance-wide MCP startup and shutdown bursts. Supports
|
||||
# MCP__LIFECYCLE_CONCURRENCY and is clamped to a maximum of 128.
|
||||
lifecycle_concurrency: 16
|
||||
stdio:
|
||||
# Independent gate for local stdio MCP transports. Cloud v2 sets
|
||||
# MCP__STDIO__ENABLED=false even when Box Runtime is available.
|
||||
enabled: true
|
||||
monitoring:
|
||||
query_limits:
|
||||
# Maximum records materialized by one paginated monitoring request.
|
||||
# Supports MONITORING__QUERY_LIMITS__PAGE_ROWS (hard cap: 5000).
|
||||
page_rows: 1000
|
||||
# CSV exports are currently assembled in memory. Keep this lower than
|
||||
# the historical 100000-row default (hard cap: 50000).
|
||||
export_rows: 10000
|
||||
# Maximum related records returned by one session/message detail view
|
||||
# (hard cap: 10000). Aggregate statistics remain database-computed.
|
||||
detail_rows: 2000
|
||||
# Token charts are grouped in SQL and return only the newest buckets
|
||||
# (hard cap: 10000). Supports an environment variable override.
|
||||
timeseries_buckets: 1000
|
||||
# Bound high-offset scans that can otherwise monopolize PostgreSQL CPU
|
||||
# (hard cap: 10000000).
|
||||
max_offset: 1000000
|
||||
auto_cleanup:
|
||||
# Enable automatic cleanup of expired monitoring records
|
||||
enabled: true
|
||||
@@ -132,6 +313,9 @@ monitoring:
|
||||
check_interval_hours: 1
|
||||
# Number of expired rows to delete per table batch
|
||||
delete_batch_size: 1000
|
||||
# Prevent one large Workspace backlog from monopolizing PostgreSQL.
|
||||
# Supports MONITORING__AUTO_CLEANUP__MAX_BATCHES_PER_TABLE_PER_RUN.
|
||||
max_batches_per_table_per_run: 4
|
||||
box:
|
||||
# Master switch for the Box sandbox runtime. When false, LangBot does NOT
|
||||
# attempt to connect to a remote Box runtime nor start a local stdio Box
|
||||
@@ -142,12 +326,42 @@ box:
|
||||
enabled: true
|
||||
backend: 'local' # 'local' (Docker/nsjail), 'docker', 'nsjail', or 'e2b'. Can be written via BOX__BACKEND.
|
||||
runtime:
|
||||
# External WebSocket runtimes also require LANGBOT_BOX_CONTROL_TOKEN in
|
||||
# both LangBot and Box. Keep the shared secret out of this config file.
|
||||
endpoint: '' # External Box Runtime base URL, e.g. 'ws://127.0.0.1:5410'. Leave empty for local auto-managed runtime.
|
||||
limits:
|
||||
max_sessions: 64 # Includes persistent sessions. New sessions fail explicitly when this cap is reached.
|
||||
max_managed_processes: 64 # Maximum concurrently running stdio MCP / managed processes.
|
||||
max_completed_processes: 256 # Global cap for retained exited-process diagnostics.
|
||||
completed_process_retention_sec: 300 # Keep exited-process diagnostics before releasing memory.
|
||||
max_sessions: 64
|
||||
max_managed_processes: 64
|
||||
max_completed_processes: 256
|
||||
# Core scans a Workspace before and after quota-enforced executions.
|
||||
# Fail closed instead of repeatedly walking an inode bomb.
|
||||
# Supports BOX__LIMITS__MAX_WORKSPACE_ENTRIES (hard cap: 1000000).
|
||||
max_workspace_entries: 100000
|
||||
# Retained admission fences prevent replay after entitlement expiry or
|
||||
# revocation. Fail closed before that monotonic state can grow without
|
||||
# bound; Cloud may override this with BOX__LIMITS__MAX_ADMISSION_RECORDS.
|
||||
max_admission_records: 100000
|
||||
max_rpc_file_bytes: 20971520
|
||||
# Cloud v2 overrides these values through the instance config/environment.
|
||||
# OSS keeps admission disabled and preserves the existing multi-session
|
||||
# local behavior. These limits are Runtime-owned and cannot be relaxed by
|
||||
# a pipeline, Workspace entitlement, or tool call.
|
||||
admission:
|
||||
required: false
|
||||
logical_session_id: 'global'
|
||||
required_backend: 'nsjail'
|
||||
max_sessions: 1
|
||||
max_managed_processes: 0
|
||||
max_grant_ttl_sec: 300
|
||||
max_timeout_sec: 120
|
||||
cpus: 1.0
|
||||
memory_mb: 512
|
||||
pids_limit: 128
|
||||
read_only_rootfs: true
|
||||
# OSS admission-disabled mode uses 0 for unlimited compatibility.
|
||||
# Cloud bootstrap requires a positive hard quota.
|
||||
workspace_quota_mb: 0
|
||||
readiness_cache_sec: 15
|
||||
local:
|
||||
profile: 'default'
|
||||
image: '' # Custom local sandbox image. Leave empty to use the profile default.
|
||||
|
||||
@@ -356,6 +356,7 @@
|
||||
isConnected: false,
|
||||
ws: null,
|
||||
connectionId: null,
|
||||
sessionToken: null,
|
||||
sessionId: getOrCreateSessionId(),
|
||||
reconnectAttempts: 0,
|
||||
heartbeatTimer: null,
|
||||
@@ -538,7 +539,12 @@
|
||||
|
||||
state.ws.onopen = function () {
|
||||
state.reconnectAttempts = 0;
|
||||
startHeartbeat();
|
||||
state.ws.send(
|
||||
JSON.stringify({
|
||||
type: "authenticate",
|
||||
token: state.sessionToken || "",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
state.ws.onmessage = function (event) {
|
||||
@@ -576,6 +582,7 @@
|
||||
state.connectionId = data.connection_id;
|
||||
if (state.hasConnected) loadHistory(true);
|
||||
state.hasConnected = true;
|
||||
startHeartbeat();
|
||||
updateStatusDot();
|
||||
updateSendBtn();
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user