From 9eb292992d881e81bccc294e79890aad411df436 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sat, 18 Jul 2026 20:24:12 +0800 Subject: [PATCH] docs: redesign multi-tenant workspace architecture --- .../workspace-multi-user-architecture.md | 2280 ++++++++++++----- 1 file changed, 1586 insertions(+), 694 deletions(-) diff --git a/docs/multi-tenant/workspace-multi-user-architecture.md b/docs/multi-tenant/workspace-multi-user-architecture.md index 3edca49d4..116b4268e 100644 --- a/docs/multi-tenant/workspace-multi-user-architecture.md +++ b/docs/multi-tenant/workspace-multi-user-architecture.md @@ -1,858 +1,1750 @@ -# LangBot 多租户与多用户改造方案 +# LangBot Workspace 多用户与 SaaS 多租户架构 -## 目标 +## 1. 决策摘要 -本方案面向 LangBot 从“单实例单管理员”演进到 SaaS 友好的“多 workspace、多账户、多权限”架构。 +本方案采用以下架构: -核心定义: +> 开源 LangBot Core 提供完整的租户隔离内核和单 Workspace 多用户能力;SaaS 使用独立闭源 Cloud Control Plane 管理全局账户、多 Workspace、计费、权益、放置和 Cloud 生命周期。 -- Account:登录主体。一个自然人或服务账号,可加入多个 workspace。 -- Workspace:租户边界。一个 workspace 内可拥有多个用户、机器人、流水线、模型、知识库、扩展、监控数据与 API Key。 -- Membership:账户与 workspace 的关系,承载角色与权限。 -- Role/Permission:workspace 内权限,不再用“是否是当前唯一用户”来决定访问能力。 +这是本次设计的核心边界: -目标体验: +- Workspace 是 LangBot 实例内的逻辑租户边界,不是一个 Pod,也不是一个 Kubernetes namespace。 +- OSS 每个 LangBot 实例只能存在一个 Workspace,但该 Workspace 可以有多个用户、邀请和固定角色权限。 +- SaaS 一个账户可以拥有或加入多个 Workspace;一个 LangBot CloudInstance 可以承载多个 Workspace。 +- SaaS 普通注册会自动创建个人 Workspace。通过邀请注册的新用户也会创建个人 Workspace,并同时加入受邀 Workspace。 +- OSS 首位用户创建实例唯一 Workspace;后续用户通过邀请加入该 Workspace,不再创建第二个 Workspace。 +- 数据隔离、权限校验、运行时隔离必须留在开源 Core 中,不能依赖闭源服务在线代理每次请求。 +- SaaS 全局账户、Workspace 目录、Membership、Invitation、Subscription、Billing、Entitlement 和 Placement 由闭源控制面维护;Core 只保存执行所需的 Account、Workspace 和 Membership 版本化投影,不投影 pending Invitation secret。 +- 现有 Space 的 Pod-per-account、namespace-per-account 和 Pod subscription 模型不进入新架构。Cloud 部署按绿地方案重新设计。 +- 现有 Space 只复用账户交互、OAuth、支付适配器、邮件、财务后台和生命周期可靠性经验,不复用旧 Cloud 领域模型。 -- 新用户登录后可以创建 workspace、加入 workspace、切换 workspace。 -- 同一个账户可加入多个 workspace,每个 workspace 权限不同。 -- 一个 workspace 可邀请多个用户协作,并分别设置 owner/admin/editor/viewer 等权限。 -- 所有业务资源默认属于某个 workspace,所有 API 默认在当前 workspace 下工作。 -- Plugin SDK、MCP、知识库、模型调用、监控日志都能拿到稳定的 workspace 上下文,并且不跨租户泄露数据。 +推荐将 Cloud Control Plane 做成独立进程、独立发布、独立数据库和独立信任域的闭源服务。第一阶段可以和 Space 放在同一个私有代码仓库中,但不能作为 LangBot Core 的 Python 闭源补丁,也不应继续把新 Cloud 逻辑堆进现有 Space 单体。 -## 调研结论 +## 2. 范围与非目标 -### 当前 LangBot 的单用户假设 +### 2.1 本方案覆盖 -LangBot 现在已经有 `users` 表和 JWT 登录,但仍是单用户/单租户模型: +- OSS 单 Workspace 多用户、邀请和固定 RBAC。 +- SaaS 多 Workspace 账户与成员模型。 +- Workspace 行级数据隔离。 +- HTTP、API Key、Bot、Webhook 和内部服务鉴权。 +- Query、Session、Plugin、MCP、RAG、Box、Storage 和 Monitoring 运行时隔离。 +- SaaS Control Plane 与 LangBot Data Plane 的协议边界。 +- Workspace 级订阅、权益、用量和计费模型。 +- 新 Cloud v2 的 Cell、CloudInstance、Placement 和 Router 架构。 +- 分阶段实施和验收策略。 -- `src/langbot/pkg/entity/persistence/user.py` 的 `User` 只保存 `user/password/account_type/space_*`,没有角色、状态、workspace 关系。 -- `src/langbot/pkg/api/http/service/user.py` 通过 `is_initialized()` 判断系统是否已有用户;`create_or_update_space_user()` 在系统已初始化且邮箱不匹配时拒绝新用户,这直接限制了多用户登录。 -- `src/langbot/pkg/api/http/controller/group.py` 的 `AuthType.USER_TOKEN` 验证后只向 handler 注入 `user_email`;JWT payload 也只有 `user`,没有 `account_id`、`workspace_id`、`role`、`permissions`。 -- `src/langbot/pkg/api/http/service/apikey.py` 的 API Key 只验证 key 是否存在,没有 owner、scope、workspace。 -- `web/src/app/infra/http/BaseHttpClient.ts` 从 `localStorage.token` 读取单个 token,并在所有请求上加 `Authorization`;前端没有 workspace selector,也没有当前 workspace 上下文。 +### 2.2 本方案不覆盖 -当前登录流程更像“初始化一个本地管理账号”,而不是 SaaS 账户体系。要支持多用户,必须把“初始化状态”和“首个 workspace 创建”拆开。 +- 兼容旧 Space 的 Pod、账户 namespace 或旧 CloudPlan 部署方式。 +- 在旧 Pod 模型上原地升级为多租户。 +- 旧 Cloud 客户数据和基础设施的具体迁移执行方案。 +- 第一版自定义角色、SAML、SCIM 或企业离线授权。 +- 第一版跨 Cell active-active。 +- 第一版 database-per-workspace 或 schema-per-workspace。 -### 业务资源当前都是全局资源 +如果未来需要迁移旧 Cloud 的账户、财务记录或客户数据,应单独立项;旧部署模型不作为新架构的设计约束。 -主要持久化表没有租户字段: +## 3. 术语与不变量 -- Bot:`bots` -- Pipeline:`legacy_pipelines`、`pipeline_run_records` -- Model:`model_providers`、`llm_models`、`embedding_models`、`rerank_models` -- Plugin:`plugin_settings` -- MCP:`mcp_servers` -- RAG:`knowledge_bases`、`knowledge_base_files`、`knowledge_base_chunks` -- Monitoring:`monitoring_messages`、`monitoring_llm_calls`、`monitoring_sessions`、`monitoring_errors`、`monitoring_embedding_calls`、`monitoring_feedback` -- API Key:`api_keys` -- Webhook:`webhooks` -- Metadata:`metadata` -- Binary storage:`binary_storages` +### 3.1 术语 -对应服务也直接 select 全表,例如: +| 术语 | 定义 | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| Account | 登录主体。OSS 中是实例本地账户;SaaS 中是全局账户 | +| Workspace | LangBot 实例内的逻辑租户;active 后属于一个 LangBot Instance,是资源、成员、权限、运行时和计费的主要边界 | +| Membership | Account 与 Workspace 的关系,包含角色、状态和权限版本 | +| Invitation | 邀请一个用户或邮箱加入 Workspace 的一次性凭证;SaaS 要求 verified email | +| LangBot Core | 开源数据面,运行 Bot、Pipeline、Model、Plugin、MCP、RAG 等业务 | +| Cloud Control Plane | 闭源 SaaS 控制面,管理全局身份、Workspace 目录、计费、权益、放置和运维 | +| CloudInstance | 一个可承载多个 Workspace 的 LangBot 逻辑部署单元,拥有独立 Manifest、Core deployment、数据 namespace、release 和 runtime ownership domain | +| Cell | 一个区域或故障域中的基础设施与运维单元,共享计算和存储集群但承载多个逻辑隔离的 CloudInstance | +| Placement | Workspace 到 Cell 和 CloudInstance 的版本化放置关系 | +| BillingAccount | 付款主体,可以为一个或多个 Workspace 付费 | +| Entitlement | 控制面签发、Core 本地执行的功能和额度快照 | -- `BotService.get_bots()` 返回所有 bot。 -- `PipelineService.get_pipelines()` 返回所有 pipeline。 -- `ModelProviderService.get_providers()` 返回所有 provider。 -- `MCPService.get_mcp_servers()` 返回所有 MCP server。 -- 插件和二进制存储没有 workspace 维度,插件 workspace storage 在 SDK 里还硬编码为 `default`。 +### 3.2 必须始终成立的不变量 -所以改造重点不是只给用户表加字段,而是给资源访问层统一加入 workspace scope。 +1. SaaS 中一个 active 或 routable Workspace 在任一时刻必须且只能属于一个有效 CloudInstance placement;provisioning、dormant 或 archived Workspace 可以暂时没有 Placement。OSS Workspace 直接属于本地 LangBot Instance,不使用 Cloud Placement。 +2. 一个 CloudInstance 可以承载多个 Workspace。 +3. OSS 一个实例最多只有一个 Workspace。 +4. Workspace 可以有多个 Account;Account 在 SaaS 可以加入多个 Workspace。 +5. 所有租户业务资源都有非空 workspace_uuid。 +6. 所有资源读取和写入都使用 workspace_uuid 与 resource_id 共同定位。 +7. Workspace 选择器不是授权凭证;服务端必须重新验证 Membership。 +8. Core 是资源访问和运行时授权的最后一道边界。 +9. Control Plane 不参与每条消息或每个普通资源请求的同步鉴权。 +10. SaaS 缺少有效 Workspace 上下文时必须失败,不能回退到第一个或最近 Workspace。 +11. 后台任务和运行时任务必须携带显式 Workspace 或受约束的 SystemContext。 +12. Workspace、Deployment 和 BillingAccount 是三个不同概念,不能再次合并为 Pod。 -### 运行时也存在全局单例假设 +Control Plane 可以先创建 status = provisioning 或 dormant 的 Workspace directory record,但它只是全局索引,不承载 LangBot 业务资源。SaaS Workspace 只有获得唯一 active Placement 后才成为可使用的实例内租户;所有 Bot、Pipeline、Plugin 等资源始终落在该 CloudInstance 内。OSS 初始化时直接在本地实例创建 active Workspace。 -`src/langbot/pkg/core/stages/build_app.py` 初始化的是一个全局 `Application`,其中包含单例: +## 4. 产品行为 -- `platform_mgr` -- `pipeline_mgr` -- `model_mgr` -- `tool_mgr` -- `plugin_connector` -- `sess_mgr` -- `rag_mgr` -- `vector_db_mgr` +### 4.1 OSS 与 SaaS 能力矩阵 -当前运行时把所有 bot、pipeline、model、plugin、MCP 都加载到同一套内存管理器。多租户改造需要决定:是共享运行时并在对象上带 workspace 过滤,还是每个 workspace 拆 runtime shard。第一阶段建议共享进程、强制 workspace-aware;等规模变大后再演进为按 workspace 分片。 +| 能力 | OSS | SaaS | +| ------------------------------------- | ---------------------------- | --------------------------------------- | +| 每个 Account 可拥有的 Workspace | 不适用;本地实例只有一个 | 由 ProductPolicy 决定 | +| 每个 CloudInstance 可承载的 Workspace | 固定 1 | 由 RuntimeClass 和 capacity policy 决定 | +| Workspace 成员数 | 多用户 | 按 Workspace 权益 | +| 邀请成员 | 支持 | 支持 | +| 固定 RBAC | 支持 | 支持 | +| 自定义角色 | 不支持 | 后续商业能力 | +| Workspace 创建 | 仅初始化时创建唯一 Workspace | 注册自动创建,也可按权益继续创建 | +| Workspace 切换 | 无需展示 | 支持 | +| 全局账户 | 无 | 支持 | +| 订阅与计费 | 无远端依赖 | 控制面管理 | +| SSO、SCIM | 不支持 | 后续商业能力 | +| 租户隔离 | 必须完整实现 | 必须完整实现 | -### Plugin SDK 对 workspace 的假设 - -SDK 当前只认识 bot/pipeline/query/session,不认识租户: - -- `src/langbot_plugin/api/entities/builtin/pipeline/query.py` 的 `Query` 有 `query_id/launcher_type/launcher_id/sender_id/bot_uuid/pipeline_uuid`,没有 `workspace_id/account_id`。 -- `src/langbot_plugin/api/entities/builtin/provider/session.py` 的 `Session` 只按 `launcher_type + launcher_id` 表达会话。 -- `src/langbot_plugin/api/proxies/langbot_api.py` 暴露 `get_bots/get_llm_models/invoke_llm/list_tools/vector_*` 等 Host API,都是全局语义。 -- `src/langbot_plugin/runtime/io/handlers/plugin.py` 的 `set_workspace_storage/get_workspace_storage` 把 `owner_type` 设为 `workspace`,但 `owner` 固定为 `default`。 -- LangBot 侧 `src/langbot/pkg/plugin/handler.py` 处理插件请求时,会把 `GET_BOTS`、`GET_LLM_MODELS`、`VECTOR_*` 等转到全局服务。 - -这意味着多租户落地时,不能只在 Web API 层过滤;插件可以通过 Host API 访问全局资源,所以 SDK/Runtime 通信也必须传递 workspace context。 - -## 开源版与商业版产品边界 - -LangBot 是开源软件,但多 workspace 能力本质上是 SaaS 控制面能力。如果把完整多 workspace、成员协作、订阅权益和配额代码都放进开源仓库,只靠本地 feature flag 或本地 license check,无法有效避免第三方 fork 后自建 SaaS。因此建议采用 open-core 架构:开源版保留单 workspace 执行能力,账户、订阅、权益和多 workspace 协作能力放到 LangBot Space/Cloud Control Plane 和商业模块中。 - -### 版本边界 - -推荐拆成三层: - -- `LangBot Core OSS`:开源,自托管,默认只有一个隐式 `default workspace`。它可以在数据结构上兼容 workspace,但产品能力上不提供创建多个 workspace、切换 workspace、成员邀请、成员权限管理、审计和多租户配额。 -- `LangBot Space / Cloud Control Plane`:托管控制面,负责 account、workspace、membership、subscription、billing、entitlement、license token、workspace quota、marketplace 权益等能力。 -- `LangBot Commercial Module`:商业闭源或私有包,承载多 workspace、团队协作、RBAC、自定义角色、审计日志、SAML/SSO、企业私有化授权等能力。 - -企业私有化版本可以采用 `LangBot Core + Commercial Module + License Token` 的形式交付。开源 Core 仍然可独立运行,但只能作为单 workspace 自托管产品,不提供 SaaS 多租户控制面。 - -### OSS 中如何保留兼容但不开放多 workspace - -为了让后续商业版复用同一套资源隔离模型,OSS 代码里可以保留 `workspace_uuid` 相关字段和默认 workspace 迁移,但应限制为单 workspace: - -- 首次初始化时创建一个 `Default Workspace`。 -- 所有资源自动归属这个 default workspace。 -- 不暴露 `POST /api/v1/workspaces`。 -- 不暴露 workspace switcher。 -- 不暴露成员邀请和成员角色管理。 -- 不支持一个 account 加入多个 workspace。 -- 不支持 workspace 数量大于 1。 -- 前端不展示 workspace selector。 -- API 层如果收到非 default workspace 的 `X-Workspace-Id`,直接拒绝。 - -也就是说,OSS 可以是 workspace-aware,但不是 multi-workspace-enabled。这样做的价值是:代码结构提前适配租户隔离,未来商业版不用重写所有资源模型;同时开源版用户无法直接通过 UI/API 获得 SaaS 型多租户能力。 - -### 账户、订阅和权益抽到 Space - -账户和订阅体系建议从 LangBot Core 中抽出,交给 Space 控制面: +OSS 的 edition policy 应表达为: ```text -LangBot Space - -> Account - -> Workspace - -> Membership - -> Subscription - -> Entitlement - -> License Token - -LangBot Core - -> Validate entitlement / license - -> Run bots, pipelines, plugins, MCP, RAG - -> Enforce local resource scope - -> Report usage +workspace_limit = 1 +members_enabled = true +invitations_enabled = true +fixed_rbac_enabled = true +multi_workspace_enabled = false ``` -这样做有几个原因: +不能再使用 member_limit = 1,也不能通过关闭邀请或关闭 RBAC 来限制 OSS。 -- 账号体系如果完全在本地,第三方容易直接改库创建 workspace/membership。 -- 订阅、配额和商业权益如果完全在本地,容易绕过。 -- Space 可以统一处理 OAuth、组织、邀请、付款、发票、套餐、权益、Marketplace 分发权限。 -- LangBot Core 只作为执行面消费 Space 下发的 entitlement,减少商业规则暴露。 +### 4.2 OSS 注册与邀请 -### Entitlement 设计 +首次初始化必须在一个数据库事务内完成: -Space 向 LangBot Core 下发签名权益,可以是在线校验,也可以为企业版提供短期/长期离线 license token。 +1. 创建本地 Account。 +2. 创建实例唯一 Workspace。 +3. 创建 owner Membership。 +4. 创建默认 Pipeline、metadata 等 Workspace 初始资源。 +5. 标记实例初始化完成。 -示例: +初始化完成后默认关闭公开注册。后续用户流程: + +1. owner 或 admin 创建 Invitation。 +2. 被邀请用户打开一次性邀请链接并完成注册。 +3. 接受 Invitation。 +4. 原子创建或激活 Membership。 +5. 用户进入实例唯一 Workspace。 + +OSS 后续注册不创建新 Workspace。这是“注册自动创建 Workspace”规则在单 Workspace edition 下唯一必要的例外。 + +OSS 不能强制依赖 SMTP。未配置邮件服务时,管理员可以生成只展示一次的一次性邀请链接并通过可信渠道发送;配置邮件后再由系统发送并校验邮箱。可以提供显式配置允许管理员直接创建本地用户或开放注册,但新用户仍只能加入唯一 Workspace,默认角色不得高于 viewer。 + +### 4.3 SaaS 普通注册 + +普通注册必须由 Control Plane 通过幂等工作流完成: + +1. 创建全局 Account 和 AuthIdentity。 +2. 创建 personal Workspace。 +3. 创建 owner Membership。 +4. 创建个人免费 Subscription 或 Entitlement。 +5. 完成 verified email、速率限制和基础风控。 +6. 首次访问需要运行资源时再为 Workspace 分配 Placement。 +7. 将 Workspace、Account 和 Membership 投影到目标 Core。 +8. 目标 Core 就绪后返回可访问 route。 + +注册请求必须带 idempotency key,任何步骤重试都不能创建重复 Account、Workspace、Subscription 或 Placement。 + +逻辑 personal Workspace directory record 可以在注册事务中立即创建,但在 Placement active 前不能创建 LangBot 业务资源,也不应为每个未活跃账户启动完整 Runtime。Control Plane 需要对未使用的免费 Workspace 懒分配、休眠和回收,防止邀请或注册风暴放大基础设施成本。 + +### 4.4 SaaS 邀请注册 + +邀请一个未注册用户时: + +1. 用户通过一次性 Invitation token 完成注册。 +2. Control Plane 创建全局 Account。 +3. 按产品要求创建该用户的 personal Workspace 和 owner Membership。 +4. 同时接受目标 Workspace 的 Invitation。 +5. 登录后默认进入受邀 Workspace。 + +如果用户已注册,接受邀请只创建目标 Membership,不再创建新的 personal Workspace。 + +个人 Workspace 建议使用独立的免费 entitlement,不自动成为团队 Workspace 的付费席位或额外付费对象。是否计入账户可拥有 Workspace 上限,应在 ProductPolicy 中显式定义,不能由代码隐式推断。 + +### 4.5 Invitation 安全规则 + +- token 使用至少 256-bit 加密安全随机数,数据库只保存 hash。 +- token 有 expires_at、accepted_at、revoked_at,并且只能使用一次。 +- 重发邀请必须撤销旧 token。 +- 同一 workspace_uuid 与 normalized_email 同时只能有一个有效邀请。 +- 接受邀请必须锁定记录,Membership 创建和 token 消费在同一事务内提交。 +- Invitation 不能授予 owner;owner 转移使用独立流程。 +- SaaS 或启用邮件验证的 OSS 中,OAuth 邮箱相同不能自动接受邀请,必须验证 token、显式确认并要求 verified email。 +- 未配置 SMTP 的 OSS 使用邀请链接本身作为高熵、单次使用的授权证明,并记录创建者、接受者、来源 IP 和审计事件。 +- Workspace 必须始终至少有一个 active owner。 +- admin 不能移除或降级 owner。 + +## 5. 当前代码现状 + +### 5.1 LangBot Core 仍是单用户模型 + +当前代码已有 users 表和 JWT,但仍围绕单管理员初始化: + +- src/langbot/pkg/entity/persistence/user.py 的 User 没有稳定 account_uuid、角色或 Workspace 关系。 +- src/langbot/pkg/api/http/service/user.py 的 JWT 仍主要使用 email 标识用户。 +- 同一服务通过 initialized guard 拒绝第二个不匹配的 Space 用户。 +- src/langbot/pkg/api/http/controller/group.py 只向 handler 注入 user_email,没有统一 RequestContext。 +- API Key 没有 Workspace 所属和 scope。 +- Web 客户端只保存 token,没有可信的当前 Workspace 上下文。 + +### 5.2 业务资源没有租户字段 + +以下资源目前以实例全局方式存取: + +- bots +- legacy_pipelines +- pipeline_run_records +- model_providers +- llm_models +- embedding_models +- rerank_models +- plugin_settings +- mcp_servers +- knowledge_bases、files、chunks +- monitoring 相关表 +- api_keys +- webhooks +- binary_storages +- metadata + +对应 Service 中仍存在直接查询全表的路径。因此只增加 Workspace API 不会形成租户隔离,必须逐表、逐服务和逐运行时完成 scope 改造。 + +### 5.3 运行时是实例级单例 + +当前 Application 中的 platform、pipeline、model、tool、plugin、session、RAG 和 vector manager 主要是进程内单例。第一阶段不能简单把现有进程扩成多个活跃副本,也不能假设加一个 workspace_uuid 字段后所有缓存和连接自然隔离。 + +### 5.4 Plugin SDK 不认识可信 Workspace + +当前 Query、Session 和 Host API 协议没有完整的 Workspace 上下文。SDK 的 workspace storage 仍使用 default owner。插件可以通过 Host API 枚举或调用全局资源,因此隔离必须进入连接、协议和 Host handler,而不只是 Web API。 + +## 6. 架构选择 + +### 6.1 推荐方案 + +采用以下三部分: + +1. LangBot Core OSS tenancy kernel。 +2. 独立闭源 Cloud Control Plane。 +3. 每个 Cell 内的 Cell Agent 和 LangBot Data Plane。 + +```mermaid +flowchart LR + User["User / API Client"] --> Edge["Global Edge & Workspace Router"] + Edge --> CP["Closed Cloud Control Plane"] + Edge --> Cell["Cloud Cell"] + CP -->|"Directory events / Entitlements / Desired state"| Cell + Cell --> Core["LangBot Core Data Plane"] + Core -->|"Usage events / Observed state"| CP + Core --> DB["Workspace-scoped Data"] + Core --> Runtime["Bot / Pipeline / Plugin / MCP / RAG Runtime"] +``` + +### 6.2 为什么使用独立闭源服务 + +独立 Control Plane 的优点: + +- 商业规则、支付密钥、集群凭证和平台管理权限不进入数据面。 +- Account 可跨多个 Cell 和 CloudInstance 加入 Workspace。 +- 订阅、权益、用量、路由和部署可以独立扩容和高可用。 +- Core 与控制面通过版本化协议耦合,不依赖 Python ABI 或同进程插件。 +- Control Plane 故障时 Core 可以使用本地投影和权益快照继续处理已有工作负载。 + +第一阶段不需要拆成大量微服务。可以先做模块化单体,但必须拥有独立部署单元和数据库。 + +### 6.3 为什么不使用 Core 内闭源模块 + +不推荐把 SaaS 多租户和计费做成 Core 内闭源模块: + +- 会把支付、全局身份和集群密钥带进数据面。 +- 与 Core 发布版本强耦合,难以独立扩容和回滚。 +- 多 Cell 路由和全局账户无法由单实例模块可靠管理。 +- 数据面被入侵时会扩大到计费和云运维信任域。 + +Core 内闭源模块可以保留为未来企业离线授权的交付形态,但不作为官方 SaaS 主架构。 + +### 6.4 为什么不能把隔离能力全部闭源 + +以下能力必须在开源 Core 中完整存在: + +- Account、Workspace、Membership、Invitation 基础模型。 +- OSS 单 Workspace edition policy。 +- 固定角色和权限映射。 +- RequestContext 和 WorkspaceScoped Repository。 +- 所有资源的 Workspace 行级隔离。 +- API Key、Webhook、Bot route 的 Workspace 绑定。 +- Plugin、MCP、RAG、Box、Session、Storage 和 Monitoring 隔离。 +- Entitlement 验签、额度执行钩子和 Usage Outbox。 +- 跨租户测试。 + +否则 OSS 多用户本身不安全,SaaS 也会把最关键的隔离边界变成网络依赖。 + +### 6.5 商业边界的现实限制 + +Core 为了安全地支持 OSS 多用户,必须包含 Workspace schema、scope 和固定 RBAC;测试环境也必须能构造多个 Workspace 验证隔离。因此技术上无法阻止第三方 fork 后移除 OSS 的 Workspace 数量限制。 + +官方 SaaS 的商业边界应建立在闭源 Control Plane、计费、全局目录、Cloud 运维、官方服务和商标授权上,而不是故意削弱 Core 的隔离能力。不要把一个本地环境变量或可修改的前端 feature flag 当作商业保护。 + +## 7. 开源与闭源职责边界 + +### 7.1 LangBot Core OSS + +Core 负责: + +- 实例本地 Account 和 AuthIdentity。 +- Workspace、Membership,以及 OSS 本地 Invitation。 +- 固定 RBAC。 +- 业务资源及其 Workspace scope。 +- 运行时资源和连接隔离。 +- 本地身份、SaaS 身份投影和 Membership 投影;SaaS pending Invitation 不进入 Core。 +- 本地 edition policy。 +- Cloud InstanceManifest 和 EntitlementSnapshot 验签。 +- 本地 quota enforcement。 +- UsageEvent durable outbox。 +- 基础安全审计事件。 + +Core 是 Bot、Pipeline、Model、Knowledge、Plugin installation、MCP configuration 和 Monitoring 数据的权威来源。 + +### 7.2 Closed Cloud Control Plane + +Control Plane 负责: + +- SaaS 全局 Account、AuthIdentity、Session、OIDC 和后续 SSO。 +- SaaS Workspace directory。 +- SaaS Membership 和 Invitation 权威状态。 +- Workspace 创建、归档、暂停和删除工作流。 +- BillingAccount、Product、PlanVersion、Price 和 Subscription。 +- Order、PaymentAttempt、ProviderEvent、Invoice 和 Refund。 +- Entitlement 计算和签名。 +- Usage ledger、聚合、额度和欠费策略。 +- Cell、CloudInstance、Placement、Route 和 Domain。 +- RuntimeClass、Release 和 DesiredState。 +- SaaS 运营后台、平台角色和高级审计。 + +Control Plane 不保存 Bot、Pipeline、Model 或 Knowledge 等业务资源,也不代理普通 Bot 消息执行。 + +### 7.3 SaaS Adapter + +Core 内只保留薄的协议适配层: + +- 验证 InstanceManifest、Account token 和 JWKS。 +- 消费 DirectoryEvent 并写入本地投影。 +- 缓存 EntitlementSnapshot。 +- 将 UsageEvent 写入 outbox。 +- 上报 ObservedState。 + +适配层不得 monkey patch ORM,不得绕过 Core 的权限检查,也不得在一次普通资源请求中同步调用 Control Plane。 + +### 7.4 Provider 接口 + +建议在 Core 定义稳定接口: + +```python +class IdentityProvider: + async def authenticate(self, credential) -> PrincipalContext: ... + +class WorkspaceDirectoryProvider: + async def resolve_membership( + self, + account_uuid: str, + workspace_uuid: str, + ) -> WorkspaceMembership: ... + +class EditionPolicyProvider: + async def can_create_workspace(self, account_uuid: str) -> bool: ... + +class EntitlementProvider: + async def get_snapshot(self, workspace_uuid: str) -> EntitlementSnapshot: ... + +class UsageSink: + async def append(self, event: UsageEvent) -> None: ... +``` + +OSS 使用 LocalProvider;SaaS 使用本地 ProjectionProvider 和签名快照,不使用每请求 RemoteProvider。 + +## 8. Source of Truth 与同步协议 + +### 8.1 Source of Truth + +| 数据 | OSS | SaaS | +| -------------------------------------------- | ------------------- | --------------------------------------- | +| Account、Workspace、Membership | Core 本地数据库 | Control Plane 权威,Core 保存版本化投影 | +| Invitation | Core 本地数据库 | Control Plane 权威,不向 Core 投影 | +| Bot、Pipeline、Model、KB、Plugin、MCP | Core | Core | +| Subscription、Payment、Invoice、Usage ledger | 无远端依赖 | Control Plane | +| Feature 和 Quota | 本地 edition policy | Control Plane 签发,Core 验签执行 | +| Placement 和 Route | 不适用 | Control Plane | +| Execution generation | Core 本地固定 epoch | Control Plane DesiredState,Core 执行 | +| 运行时授权 | Core | Core 根据本地投影和 entitlement 执行 | + +SaaS 中不是两套并行真相。Control Plane 的 directory 是权威写模型;Core 的 Account、Workspace 和 Membership 是带 revision 的本地读模型和执行模型。 + +### 8.2 DirectoryEvent + +Control Plane 通过 transactional outbox 发布: + +- account.upserted +- workspace.upserted +- workspace.status_changed +- membership.upserted +- membership.removed + +SaaS Invitation 创建、撤销和接受只在 Control Plane directory 内流转;接受成功后发布最终 membership.upserted。Core 不接收 pending Invitation、email 或 token hash。 + +每个事件至少包含: + +```text +event_id +event_type +aggregate_id +aggregate_revision +instance_sequence +instance_uuid +workspace_uuid +occurred_at +payload +schema_version +``` + +目录事件只表达 Account、Workspace 和 Membership。Placement、release 和 runtime capacity 只进入 DesiredState 流,不能由两个 revision 体系同时修改。 + +传输与恢复要求: + +- Control Plane outbox 向受认证的 broker 发布,或通过 mTLS HTTPS 发送签名 event batch。 +- Broker 使用 producer ACL;event batch 使用由 InstanceManifest 授权的 JWS key 签名并绑定 audience。 +- 以 workspace_uuid 为主要 partition key;Account 事件按 account_uuid 分区。 +- Control Plane 为每个目标 instance_uuid 维护 gap-free 的逻辑 directory log offset,作为 instance_sequence;不能直接假设 PostgreSQL sequence 或预分配自增 ID 无空洞。 +- 顺序只保证到 aggregate,Core 以 aggregate_revision 判断新旧,不依赖全局顺序。 +- Core 使用 inbox 按 event_id 去重,只接受比本地 revision 更新的事件。 +- Core 同时追踪 instance_sequence 缺口,只把无缺口的最大连续序号记为 contiguous_applied_sequence。 +- 因 aggregate_revision 过旧而业务 no-op 的已验签事件,仍要在 inbox 事务提交后把该 instance_sequence 标记为 processed。 +- 删除使用 tombstone,不依赖物理删除顺序。 +- 新实例先导入带 high_watermark 的全量 snapshot,再从该 cursor 消费增量。 +- 断流或 cursor 超出保留期时重新做 snapshot reconciliation。 +- schema_version 采用向后兼容演进;移除字段必须经过双读和版本退役窗口。 +- Core 持久化 projection readiness 和 applied watermark,并向 Control Plane ACK。 +- 注册、邀请接受和 Placement 切换只有在目标 Core 的 contiguous_applied_sequence 达到要求水位后才返回 ready route。 + +Control Plane 周期性签发两种短期 lease: + +- InteractiveDirectoryLease:约束交互账户和 Membership 授权,TTL 较短。 +- WorkspaceStatusLease:约束 Bot、Webhook、Workspace API Key 等自动化工作负载能否继续运行,TTL 可以稍长但必须有硬上限。 + +两种 lease 的 JWS protected header 都包含 alg、kid 和 typ,payload 至少包含: + +```text +iss +aud = langbot-instance:{instance_uuid} +sub = {instance_uuid} +jti +iat +nbf +exp +instance_uuid +min_required_sequence +schema_version +keyset_revision +``` + +Core 只有在 projection_ready = true 且 contiguous_applied_sequence 大于等于 min_required_sequence 时才能接受 lease。投影落后时先等待同步或 reconciliation,超过请求时限后 fail closed。Lease 只证明投影新鲜度,不替代具体 Membership 或 Workspace status 检查。 + +instance_sequence 只在 committed outbox logical log 中形成。如果底层实现发生保留号、回滚或废弃,必须写入可验签的 no-op 或 skip record;snapshot reconciliation 也可以原子重置到一个明确 high_watermark,不能留下永久空洞。 + +Lease signer 只能把已经提交且可从 directory log 连续重放的 committed watermark 写入 min_required_sequence,不能使用数值最大的已分配 ID,也不能为尚未可重放的状态签发新鲜度证明。 + +重复、乱序、延迟、断流和全量重放都必须安全。 + +### 8.3 InstanceManifest + +仅配置 system.edition = cloud 不足以启用 SaaS 多 Workspace。Core 镜像必须内置官方 root public key bundle,或在部署时通过独立安全通道注入 pinned root key 和 issuer。该信任根不能来自待验的 Manifest。 + +InstanceManifest 由 root key 签名,再授权 Gateway assertion、DirectoryEvent、DirectoryLease 和 Entitlement 使用的下级 issuer 与 JWKS。公网 Account token 的信任配置属于 Edge 全局身份配置,不由 Data Plane Manifest 授权。JWS protected header 至少包含 alg、kid 和 typ;kid 不放在业务 payload 中。 + +```text +iss +aud = langbot-instance:{instance_uuid} +sub = {instance_uuid} +jti +iat +nbf +exp +instance_uuid +cell_uuid +gateway_assertion_issuer +gateway_jwks_uri +directory_event_issuer +directory_event_audience +directory_jwks_uri +entitlement_issuer +entitlement_jwks_uri +keyset_revision +release +capabilities +tenant_isolation_version +generation +``` + +Manifest 校验要求: + +- 使用 out-of-band root bundle 验证签名。 +- aud 与目标 instance_uuid 精确匹配。 +- iss、sub、nbf、exp、jti 和 generation 有效。 +- 默认只允许很小的可配置时钟偏差。 +- root 和 delegated key 轮换使用重叠有效期与递增 keyset_revision。 +- 紧急撤销通过 root-signed key revocation manifest,并限制本地缓存最长期限。 +- Manifest 续签应在有效期过半前完成;其 exp 必须覆盖计划中的最大 Entitlement grace 和安全余量。 + +Manifest 缺失、签名错误、audience 不匹配或 generation 回滚时,SaaS 实例必须 fail closed,不能自动降级成 OSS default Workspace。 + +### 8.4 EntitlementSnapshot + +建议使用非对称签名 JWS。Protected header 至少包含 alg、kid 和 typ;payload 至少包含标准 claims 和业务字段: ```json { - "edition": "oss", - "workspace_limit": 1, - "member_limit": 1, - "multi_workspace": false, - "rbac": false, - "audit_log": false, - "custom_roles": false, - "sso": false, - "commercial_use": false, - "expires_at": 1893456000 + "iss": "cloud_control_plane", + "aud": "langbot-instance:instance_uuid", + "sub": "workspace_uuid", + "jti": "snapshot_uuid", + "iat": 0, + "nbf": 0, + "exp": 0, + "instance_uuid": "instance_uuid", + "workspace_uuid": "workspace_uuid", + "subject_type": "workspace", + "plan_revision": 12, + "entitlement_revision": 42, + "status": "active", + "features": { + "box": true, + "custom_roles": false + }, + "limits": { + "members": 20, + "bots": 50, + "pipelines": 100, + "storage_bytes": 107374182400 + }, + "grace_until": 0 } ``` -OSS 默认权益: +Core 只信任当前有效 Manifest 授权的 entitlement issuer 和 key,校验 issuer、audience、subject、instance_uuid、revision、nbf、exp 和 signature。旧 revision 不得覆盖新快照。now 超过 exp 后不能继续视为 active,只能按 grace_until 进入受限的已有工作负载宽限模式。Entitlement 的 grace 不延长 Account token 或 Membership authorization lease。 -- `workspace_limit = 1` -- `member_limit = 1` -- `multi_workspace = false` -- `rbac = false` -- `audit_log = false` -- `sso = false` +### 8.5 UsageEvent -Cloud/Pro/Enterprise 权益: +Usage 采用 append-only、至少一次投递: -- `workspace_limit > 1` -- `member_limit > 1` -- `multi_workspace = true` -- `rbac = true` -- 可按套餐打开 audit、custom roles、SSO、usage reporting、enterprise support 等能力。 - -Core 执行面需要在关键入口强制校验 entitlement: - -- 创建 workspace。 -- 邀请成员。 -- 修改成员角色。 -- 切换 workspace。 -- 创建超过 quota 的资源。 -- 开启商业模块功能。 - -### 商业模块边界 - -以下能力不建议进入 OSS 仓库的完整实现: - -- 多 workspace 创建和切换。 -- Workspace 成员邀请。 -- Workspace RBAC 和自定义角色。 -- Workspace 审计日志。 -- Workspace 级用量和配额管理。 -- 订阅、账单、发票。 -- 企业 SSO/SAML/OIDC。 -- 在线/离线 license 管理。 -- 多租户 SaaS 运营控制台。 - -OSS 仓库可以保留接口占位、默认 workspace 兼容和必要的数据隔离字段,但完整交互、管理 UI、权益校验器和多 workspace policy engine 应由 Space 或商业模块提供。 - -### 防自建 SaaS 的现实边界 - -技术上无法 100% 阻止别人 fork 开源代码后自行改造。更可靠的策略是组合: - -- 不把完整商业多 workspace 实现放进 OSS。 -- Space 控制面提供账号、订阅、权益、Marketplace 和官方托管能力。 -- 商业模块闭源或私有分发。 -- 使用商标、云服务条款和商业 license 限制“自称官方 LangBot SaaS”或未经授权商用托管。 -- 如果当前开源 license 对托管商用限制不足,需要单独评估 license 策略,必要时引入 open-core license 或新增商业附加条款。具体 license 调整需要法律评审。 - -结论:多 workspace 的底层 schema 可以在 OSS 中以 default workspace 兼容方式铺路,但多 workspace 产品能力、账户订阅权益、协作管理和 SaaS 控制面应放到 Space/商业模块,不作为开源版可直接使用的能力。 - -## 推荐总体架构 - -采用“单实例多 workspace,资源行级隔离,运行时上下文隔离”的架构: - -```mermaid -flowchart TD - A["Account"] --> B["WorkspaceMembership"] - B --> C["Workspace"] - C --> D["Bots"] - C --> E["Pipelines"] - C --> F["Models & Providers"] - C --> G["Knowledge Bases"] - C --> H["Extensions: Plugins / MCP"] - C --> I["API Keys & Webhooks"] - C --> J["Monitoring"] - D --> K["Runtime Query"] - E --> K - K --> L["Plugin Runtime"] - K --> M["MCP Runtime"] - L --> N["Workspace-scoped Host APIs"] +```text +event_id +workspace_uuid +instance_uuid +placement_generation +meter +quantity_integer +unit +source +occurred_at +entitlement_revision +schema_version ``` -原则: +Control Plane 按 event_id 去重,并记录 received_at。Data Plane 不提交 billing period、price 或最终金额;中央 ledger 根据 occurred_at、不可变 Subscription/Price version 和迟到事件策略推导账期与价格。entitlement_revision 只用于审计事件发生时执行的权益版本。 -- 账户全局唯一,workspace 是所有业务资源的归属边界。 -- 所有 HTTP handler 在进入业务服务前解析出 `RequestContext(account, workspace, membership, permissions)`。 -- 所有 service 方法显式接收 `ctx` 或 `workspace_id`,禁止在业务服务里无条件 select 全表。 -- 运行时对象的 key 从 `uuid` 扩展为 `(workspace_id, uuid)` 或使用全局唯一 uuid 但必须记录 workspace_id 并校验。 -- 插件/MCP/知识库/模型调用都按 query 所属 workspace 过滤可用资源。 +Core 不在普通请求中同步扣费。未来若需要本地实时额度,可以设计签名 BudgetLease,但不属于 Cloud v2 MVP;模型 token 如果经过中央模型网关,可由网关作为权威计量来源。 -## 数据模型设计 +### 8.6 DesiredState 与 ObservedState -### Account +Cell Agent 拉取或接收版本化 DesiredState,并幂等 reconcile: -替代现有 `users` 的语义,建议保留表名但升级字段,避免过大迁移: +- CloudInstance release。 +- RuntimeClass。 +- capacity。 +- route。 +- placement generation。 -字段建议: +Cell Agent 上报 ObservedState。Space Web API 或 Control Plane Web API 不直接持有并使用集群管理员凭证执行任意 K8s CRUD。 -- `id` -- `uuid` -- `email` -- `password_hash` -- `display_name` -- `avatar_url` -- `account_type`: `local | space` -- `status`: `active | disabled | deleted` -- `space_account_uuid` -- `space_access_token` -- `space_refresh_token` -- `space_access_token_expires_at` -- `space_api_key` -- `created_at` -- `updated_at` +Workspace 的安全或管理员状态只由 Directory aggregate 的 revision 决定;订阅状态只由 Entitlement revision 决定;Placement 只由 DesiredState generation 决定。Core 计算三者的最严格有效状态,但任何通道都不能修改另一个通道的字段。 -兼容策略: +## 9. 身份、鉴权与请求上下文 -- 旧字段 `user` 迁移为 `email`,可以短期保留 alias。 -- 旧 `password` 迁移为 `password_hash`,也可先保持列名不变,service 层改命名。 -- JWT 中不要继续只放 email,应放 `sub=account_uuid`。 +### 9.1 上下文分层 -### Workspace - -新增 `workspaces`: - -- `uuid` -- `name` -- `slug` -- `avatar_url` -- `type`: `personal | team` -- `status`: `active | suspended | deleted` -- `default_language` -- `created_by_account_uuid` -- `created_at` -- `updated_at` - -每个账户首次登录时自动创建一个 personal workspace。旧单用户实例迁移时创建一个 `Default Workspace`。 - -### WorkspaceMembership - -新增 `workspace_memberships`: - -- `workspace_uuid` -- `account_uuid` -- `role`: `owner | admin | developer | operator | viewer` -- `status`: `active | invited | disabled` -- `invited_by_account_uuid` -- `joined_at` -- `created_at` -- `updated_at` - -唯一索引: - -- `(workspace_uuid, account_uuid)` - -### WorkspaceInvitation - -新增 `workspace_invitations`: - -- `uuid` -- `workspace_uuid` -- `email` -- `role` -- `token_hash` -- `expires_at` -- `accepted_at` -- `created_by_account_uuid` -- `created_at` - -用于邀请外部用户加入 workspace。Space OAuth 登录时可以根据 email 自动匹配未接受邀请。 - -### Role 与 Permission - -先用固定角色,后续再做自定义角色。 - -建议权限: - -- `workspace.manage` -- `member.view` -- `member.invite` -- `member.update_role` -- `member.remove` -- `bot.view` -- `bot.manage` -- `pipeline.view` -- `pipeline.manage` -- `model.view` -- `model.manage` -- `knowledge.view` -- `knowledge.manage` -- `extension.view` -- `extension.manage` -- `monitoring.view` -- `apikey.manage` -- `webhook.manage` -- `billing.view` -- `billing.manage` - -角色映射: - -| Role | 说明 | 权限 | -| --- | --- | --- | -| owner | workspace 拥有者 | 全部权限;可转让 owner;不可被其他角色移除 | -| admin | 管理员 | 除 owner 转让和删除 workspace 外的全部权限 | -| developer | 构建者 | 管理 bot、pipeline、model、knowledge、extension、webhook,可看监控 | -| operator | 运营者 | 查看和启停 bot、查看监控、查看配置,不可改密钥和删除资源 | -| viewer | 只读成员 | 只读资源和监控 | - -### 业务资源加 workspace_uuid - -以下表需要新增 `workspace_uuid`: - -- `bots` -- `legacy_pipelines` -- `pipeline_run_records` -- `model_providers` -- `llm_models` -- `embedding_models` -- `rerank_models` -- `plugin_settings` -- `mcp_servers` -- `knowledge_bases` -- `knowledge_base_files` -- `knowledge_base_chunks` -- `monitoring_*` -- `api_keys` -- `webhooks` -- `binary_storages` -- `metadata` - -索引建议: - -- 所有资源表加 `(workspace_uuid, created_at)` 或 `(workspace_uuid, updated_at)`。 -- 资源唯一键从单列改为 workspace 复合唯一: - - `bots.uuid` 可保持全局唯一,但查询仍必须带 workspace。 - - `plugin_settings` 主键从 `(plugin_author, plugin_name)` 改为 `(workspace_uuid, plugin_author, plugin_name)`。 - - `mcp_servers.name` 如果未来要求唯一,必须是 `(workspace_uuid, name)`。 - - `metadata.key` 改为 `(workspace_uuid, key)`,系统级 metadata 单独放 `system_metadata` 或使用 `workspace_uuid=NULL`。 - - `binary_storages.unique_key` 建议改为 `workspace_uuid + owner_type + owner + key` 的 hash。 - -### API Key - -API Key 必须归属于 workspace: - -- `workspace_uuid` -- `created_by_account_uuid` -- `scopes` -- `expires_at` -- `last_used_at` -- `status` - -验证 API Key 后生成 `RequestContext`: - -- `account_uuid=None` 或 service account uuid -- `workspace_uuid=key.workspace_uuid` -- `permissions=key.scopes` - -这样 `/api/v1/platform/bots//send_message` 之类接口不会跨 workspace 操作 bot。 - -## 后端改造方案 - -### RequestContext - -新增统一上下文对象,例如: +建议分为四层: ```python -class RequestContext: +class PrincipalContext: account_uuid: str | None + api_key_uuid: str | None + principal_type: str + +class WorkspaceContext: workspace_uuid: str + membership_uuid: str | None role: str | None - permissions: set[str] - auth_type: Literal["user_token", "api_key"] + permissions: frozenset[str] + membership_revision: int + +class RequestContext: + instance_uuid: str + placement_generation: int + request_id: str + auth_type: str + principal: PrincipalContext + workspace: WorkspaceContext + entitlement_revision: int + +class ExecutionContext: + instance_uuid: str + workspace_uuid: str + placement_generation: int + bot_uuid: str | None + pipeline_uuid: str | None + query_uuid: str | None + trigger_principal: PrincipalContext | None ``` -改造 `RouterGroup.route()`: +服务方法不得通过全局变量或最近 Workspace 获取上下文。Gateway 的签名内部 assertion、直接进入 Core 的本地请求、后台任务、Plugin Host 调用和 runtime lease 都必须生成或继承当前 placement_generation。 -- `USER_TOKEN`:验证 JWT,读取 `account_uuid`,再从 header/query/cookie 中解析 current workspace。 -- `API_KEY`:验证 API Key,直接得到 workspace。 -- `USER_TOKEN_OR_API_KEY`:两者都返回同一种 `RequestContext`。 -- handler 参数从可选 `user_email` 升级为可选 `ctx`;兼容期同时支持 `user_email`。 +### 9.2 Account token -当前 workspace 传递方式: - -- 推荐 header:`X-Workspace-Id: ` -- Web 前端同时把当前 workspace 存在 localStorage。 -- 如果未传,后端用账户最近使用 workspace 或第一个 active membership。 - -JWT payload: +SaaS Account access token 使用非对称签名和短 TTL,至少包含: ```json { "sub": "account_uuid", - "email": "user@example.com", - "iss": "LangBot-...", - "exp": 1234567890 + "iss": "cloud_control_plane", + "aud": "langbot_cloud_edge", + "iat": 0, + "exp": 0, + "jti": "token_uuid", + "token_version": 3 } ``` -不要把 workspace 写死在 JWT 里,否则切换 workspace 需要刷新 token。可以额外支持短 TTL workspace token,但第一阶段不必。 +JWT 表示全局账户身份,不直接授予任何 Workspace 权限,也不需要因切换 Workspace 重新签发。公网 Account token 只发给 Edge 或 Cloud BFF,Data Plane 不直接接受该 audience。 -### 服务层改造模式 +OSS token 同样应从 email 主体迁移到 account_uuid,并保留 issuer、audience、expiry 和 token version。 -所有 service 方法增加 `ctx` 或 `workspace_uuid`: +### 9.3 Workspace 选择 + +浏览器和 REST 客户端可发送: + +```http +X-Workspace-Id: workspace_uuid +``` + +规则: + +- 该 header 只是选择器,不是授权证明。 +- SaaS Workspace 资源接口缺少选择器时返回 workspace_required。 +- SaaS 不得自动选择最近或第一个 Workspace。 +- OSS 可以因为实例只有一个 Workspace 而省略 header,后端解析到唯一 Workspace。 +- API endpoint 路径中的 workspace_uuid 仍必须与 RequestContext 一致。 + +公网 Edge 可以使用外部 header 或稳定 route 选择 Workspace,但必须移除客户端提供的内部上下文 header。Edge 只验证 Account token、route 和 Placement generation,不同步调用 Control Plane 验证 Membership;Membership 的最终判定只由 Core 本地投影完成。 + +Edge 通过 mTLS 注入短期、instance-scoped 的签名内部 assertion,至少包含: + +```text +iss = gateway_assertion_issuer +aud = langbot-instance:{instance_uuid} +sub = account_uuid +jti +iat +exp +auth_type +workspace_uuid +instance_uuid +placement_generation +directory_min_sequence +source_token_jti +``` + +Edge 的 route projection 由 Control Plane 异步发布,并使用与 Core 相同的 directory sequence 坐标;directory_min_sequence 只要求 Core 已追平到该水位,不代表 Membership 已通过。Core 验证 assertion issuer、audience、签名、短 TTL、Workspace、Placement generation 和投影水位,然后再加载 Membership。 + +如果未来 Edge 为了提前拒绝无权限请求而缓存 Membership,该缓存只能作为优化,不能成为放行依据,并且必须遵守相同的 watermark 与 lease 规则。 + +### 9.4 API Key、Bot 和 Webhook + +API Key: + +- 绑定 workspace_uuid。 +- 绑定 scopes、status、expires_at。 +- 数据库只保存 key hash。 +- 可选绑定 service_account_uuid。 +- 不能通过 header 切换到其他 Workspace。 + +Bot/Webhook: + +- 通过不可猜的 route token、bot_uuid 或发布记录反查 Workspace。 +- 不信任公网传入的 workspace_uuid。 +- webhook route token 可轮换和撤销。 +- Workspace slug 只用于展示,不能作为授权依据。 + +### 9.5 错误语义 + +- 资源不存在或属于其他 Workspace:返回 404。 +- 资源属于当前 Workspace但权限不足:返回 403。 +- SaaS 缺少 Workspace:返回 400 workspace_required。 +- Membership inactive 或 Workspace suspended:返回 403。 +- Entitlement 不允许新建或超限:返回明确的 edition_limit 或 quota_exceeded。 + +## 10. Core 数据模型 + +### 10.1 Account + +升级现有 users 语义,建议字段: + +- uuid +- email_normalized +- display_name +- avatar_url +- status +- source: local 或 cloud_projection +- projection_revision +- created_at +- updated_at + +OSS 本地凭证单独放在 local_credentials: + +- account_uuid +- password_hash +- password_updated_at +- token_version + +OAuth 身份不要只按 email 绑定,本地模式新增 auth_identities: + +- provider +- provider_subject +- account_uuid +- email_at_link_time +- created_at + +唯一键为 provider 与 provider_subject。 + +SaaS 投影直接使用 Control Plane 的全局 Account UUID,不再维护另一套 external_account_uuid。投影只包含显示身份和授权所需状态,绝不包含 password_hash、OAuth refresh token、浏览器 session secret 或支付凭证。 + +### 10.2 Workspace + +新增 workspaces: + +- uuid +- instance_uuid +- name +- slug +- type: personal 或 team +- status: provisioning、active、suspended、archived、deleted +- created_by_account_uuid +- source: local 或 cloud_projection +- projection_revision +- created_at +- updated_at + +OSS 数据库必须用约束和 edition policy 保证一个实例只有一个 Workspace。SaaS Workspace 必须与签名 InstanceManifest 的 instance_uuid 一致。 + +dormant 是 Control Plane directory 的未放置状态,不写入 Core workspaces。开始 Placement 后,目标 Core 才收到 provisioning Workspace 投影;Placement active 后再切换为 active。 + +### 10.3 WorkspaceExecutionState + +Core 本地新增 workspace_execution_states,统一表达当前实例可接受的写入 generation: + +- workspace_uuid +- instance_uuid +- active_generation +- state: provisioning、active、migrating、draining、inactive +- write_fenced +- source: local 或 cloud +- desired_state_revision +- updated_at + +OSS 使用 LocalExecutionStateProvider,在初始化事务中创建 generation = 1、state = active、write_fenced = false 的固定本地状态,不消费 Cloud DesiredState。SaaS 使用 CloudExecutionStateProvider,由 DesiredState consumer 更新,不由用户 API 修改。 + +每个 Unit of Work 或 Repository 写入口都必须验证: + +- RequestContext 或 ExecutionContext 的 placement_generation 等于 active_generation。 +- write_fenced = false。 +- state 允许当前操作。 + +校验必须与提交形成同一个事务协议,不能只在事务开始前 check: + +- 普通写事务对 WorkspaceExecutionState 行持有 shared transaction lock,验证 generation 后一直持有到 commit。 +- fence 操作获取 exclusive lock,等待已有写事务提交或回滚,再设置 write_fenced 和新 generation。 +- PostgreSQL 可以使用行锁或 transaction advisory lock;SQLite 使用单写事务和显式 maintenance fence 实现同一语义。 +- DB outbox 与业务写入同事务保存 operation_id 和 generation。 +- Object Store、平台消息、Plugin Host 等不可回滚副作用由 outbox dispatcher 执行,并在实际副作用前再次校验 generation。 +- Object Store 大文件先写 generation-scoped staging key,校验后再发布最终引用;旧 generation 的 staging data 可回收。 +- 无法让外部平台原生校验 fencing token 时,dispatcher 必须保证单 owner,并在切换前 drain。 + +旧 generation 即使仍能连接数据库也不能提交写入或发出新副作用。读请求是否允许在 draining 阶段继续,由迁移策略显式决定。 + +### 10.4 WorkspaceMembership + +新增 workspace_memberships: + +- uuid +- workspace_uuid +- account_uuid +- role +- status: active、disabled、removed +- invited_by_account_uuid +- joined_at +- projection_revision +- created_at +- updated_at + +唯一键: + +- workspace_uuid 与 account_uuid。 + +角色先固定为: + +| Role | 说明 | +| --------- | ------------------------------------------------- | +| owner | Workspace 所有者,可转让 owner | +| admin | 管理成员和绝大多数资源,不能处理 owner | +| developer | 管理 Bot、Pipeline、Model、Knowledge、Plugin、MCP | +| operator | 运行和观察工作负载,不能读取或修改敏感密钥 | +| viewer | 只读非敏感资源和监控 | + +固定权限矩阵: + +| Permission | owner | admin | developer | operator | viewer | +| ---------------------- | ----- | ------------------ | --------- | -------- | ------ | +| workspace.view | 是 | 是 | 是 | 是 | 是 | +| workspace.update | 是 | 是 | 否 | 否 | 否 | +| workspace.delete | 是 | 否 | 否 | 否 | 否 | +| owner.transfer | 是 | 否 | 否 | 否 | 否 | +| member.view | 是 | 是 | 是 | 是 | 是 | +| member.invite | 是 | 是 | 否 | 否 | 否 | +| member.update_role | 是 | 是,不能操作 owner | 否 | 否 | 否 | +| member.remove | 是 | 是,不能操作 owner | 否 | 否 | 否 | +| resource.view | 是 | 是 | 是 | 是 | 是 | +| resource.manage | 是 | 是 | 是 | 否 | 否 | +| runtime.operate | 是 | 是 | 是 | 是 | 否 | +| provider_secret.manage | 是 | 是 | 是 | 否 | 否 | +| api_key.manage | 是 | 是 | 否 | 否 | 否 | +| audit.view | 是 | 是 | 否 | 否 | 否 | +| data.export | 是 | 是 | 否 | 否 | 否 | +| billing_link.manage | 是 | 否 | 否 | 否 | 否 | + +resource 代表 Bot、Pipeline、Model、Knowledge、Plugin、MCP 和 Webhook;具体 Service 可以再拆细粒度 permission,但默认映射不得放宽。Monitoring 普通查看属于 resource.view,安全审计属于 audit.view。 + +权限映射在 Core 中有单一代码定义。自定义角色后续由控制面下发角色策略,但 Core 仍负责执行。Workspace role 不授予 BillingAccount 或平台管理权限。 + +### 10.5 WorkspaceInvitation + +新增 workspace_invitations: + +- uuid +- workspace_uuid +- normalized_email +- role +- token_hash +- status +- expires_at +- accepted_at +- revoked_at +- created_by_account_uuid +- created_at +- updated_at + +该表只用于 OSS 本地邀请,是本地权威表。SaaS Invitation 只存在于 Control Plane;Data Plane 不保存 pending email、token 或 token hash,接受邀请后只接收 Membership 投影。 + +### 10.6 业务资源 + +以下表新增非空 workspace_uuid: + +- bots +- legacy_pipelines +- pipeline_run_records +- model_providers +- llm_models +- embedding_models +- rerank_models +- plugin_settings 或 plugin_installations +- mcp_servers +- knowledge_bases +- knowledge_base_files +- knowledge_base_chunks +- monitoring 相关表 +- api_keys +- webhooks +- binary_storages +- Workspace metadata + +建议: + +- 每个租户表都有 workspace_uuid 与 created_at 或 updated_at 的索引。 +- 父子表使用包含 workspace_uuid 的复合外键。 +- 关联关系使用 workspace_uuid、parent_uuid、child_uuid 共同约束。 +- 全局唯一 uuid 仍保留,但所有 Repository 查询必须同时带 workspace_uuid。 +- plugin_settings 唯一键改为 workspace_uuid、plugin_author、plugin_name。 +- mcp_servers 的名称只在 Workspace 内唯一。 +- binary storage key 必须包含 instance_uuid、workspace_uuid、owner_type、owner 和 key。 + +metadata 拆为: + +- system_metadata:实例版本、迁移和全局运行信息。 +- workspace_metadata:wizard、页面设置和 Workspace 配置。 + +### 10.7 API Key + +新增或升级字段: + +- uuid +- workspace_uuid +- created_by_account_uuid +- key_hash +- scopes +- status +- expires_at +- last_used_at +- created_at + +当前全局 API Key 在 SaaS 模式必须禁用。 + +### 10.8 防御性数据库约束 + +- 所有租户表 workspace_uuid 为 NOT NULL。 +- 复合外键阻止跨 Workspace 关联。 +- owner 最后一个成员保护使用事务锁和服务层约束。 +- SaaS 投影 revision 单调增加。 +- PostgreSQL RLS 可以在后续作为 defense-in-depth,但不能替代 Repository 和 Service 权限检查。 + +## 11. Control Plane 数据模型 + +建议按领域拆分。 + +### 11.1 Identity 与 Directory + +```text +accounts +auth_identities +sessions +workspaces +workspace_memberships +workspace_invitations +workspace_routes +``` + +平台管理员角色和 Workspace 角色必须彻底分开。 + +### 11.2 Billing + +```text +billing_accounts +billing_account_memberships +billing_account_workspaces +products +plan_versions +prices +plan_features +subscriptions +subscription_items +orders +order_items +payment_attempts +provider_events +invoices +refunds +credit_ledger +``` + +Payment provider webhook 先按唯一 provider_event_id 持久化,再通过 outbox 异步履约。支付事务中不能直接创建 Deployment。 + +### 11.3 Entitlement 与 Usage + +```text +entitlement_grants +entitlement_snapshots +usage_events +usage_aggregates +budget_leases +``` + +### 11.4 Cloud + +```text +cells +cloud_instances +workspace_placements +workspace_routes +runtime_classes +releases +deployment_operations +domains +desired_states +observed_states +outbox_events +inbox_events +audit_logs +``` + +Space 已有 Instance 模型表达 OAuth 连接记录,新模型应明确命名为 CloudInstance 或 RuntimeShard,不能复用旧名字造成语义冲突。 + +## 12. Billing 与权益设计 + +### 12.1 计费主体 + +推荐: + +- Subscription 的主要 subject 是 Workspace。 +- BillingAccount 是付款主体。 +- 一个 BillingAccount 可以为多个 Workspace 付款。 +- Account 级权益控制可拥有或新建的 Workspace 数量。 +- Workspace 级权益控制成员、Bot、Pipeline、Plugin、MCP、Token、Storage 等限额。 + +这样团队 Workspace 可以独立升级、暂停或迁移,同时企业客户可以统一付款。 + +BillingAccount 使用独立 Membership 和角色: + +| Billing role | 能力 | +| -------------- | ------------------------------------------------------ | +| owner | 管理付款方式、成员、Workspace 绑定、订阅、发票和所有权 | +| billing_admin | 管理付款方式、订阅和发票,不能转移所有权 | +| billing_viewer | 只读账单、发票和用量 | + +将 Workspace 绑定到 BillingAccount 必须同时满足 Workspace 侧 billing_link.manage 授权和 BillingAccount 侧 owner 或 billing_admin 授权。Workspace admin 不会因为角色而自动看到同一 BillingAccount 下其他 Workspace 的发票。平台管理员角色同样独立,并且所有 support 操作需要审计。 + +### 12.2 Personal Workspace + +SaaS 注册自动创建的 personal Workspace 建议默认绑定免费 entitlement: + +- 不因接受团队邀请而产生额外账单。 +- 可以限制 Bot、模型额度和成员数。 +- 是否可邀请其他用户由产品计划决定。 +- 升级 personal Workspace 时创建或绑定 BillingAccount。 + +### 12.3 Subscription 状态 + +建议状态机: + +```text +trialing -> active +trialing -> expired +active -> past_due -> grace -> suspended +past_due -> active +grace -> active +suspended -> active +active -> canceled_at_period_end -> canceled +canceled_at_period_end -> active +``` + +past_due、grace 或 suspended 只有在支付恢复、ProviderEvent 已幂等入账并重新签发 Entitlement 后才回到 active。canceled_at_period_end 可以在当前 period 结束前撤销;已经 canceled 的订阅重新购买时创建新 Subscription 或新 versioned lifecycle,不原地篡改历史。 + +Subscription 是权益来源,PaymentOrder 只是一次交易。Product、PlanVersion 和 Price 一旦用于账单就不可变,只能新建版本。不得继续用订单成功或 Pod 到期日期直接代表订阅真相。 + +### 12.4 产品权益与部署规格分离 + +必须拆开: + +- Product、PlanVersion、Price、Entitlement:用户购买什么。 +- RuntimeClass、CapacityProfile、Release:平台如何部署。 +- Placement isolation_tier:shared 或 dedicated。 + +同一 Product 可以随容量策略变化而放在不同 RuntimeClass;独享实例只是 placement policy,不是新的租户模型。 + +## 13. 现有 Space 的复用边界 + +对最新 Space 架构的结论是:外围能力可复用,旧 Cloud 核心模型不可复用。 + +本次审查基于 langbot-space origin/main 4c1de05778f3,关键事实锚点: + +- internal/entities/persistence/account.go:AccountResources 仍以 MaxPods 等账户级资源建模。 +- internal/entities/persistence/payment.go:PaymentOrder 直接包含 PlanType、BillingCycle、PodUUID 和 PodSubdomain。 +- internal/entities/persistence/cloud_plan.go:CloudPlan 同时保存价格、产品限制、镜像、CPU、内存和 PVC。 +- internal/entities/persistence/pod.go:Pod 同时保存账户、部署和订阅状态。 +- internal/service/cloud.go:namespace 仍由 Account UUID 派生,Pod 删除路径会操作账户 namespace。 +- internal/controller/tasks/lifecycle_consumer.go:retry、heartbeat、generation、fencing 和 reconcile 模式值得复用。 + +### 13.1 复用与重构矩阵 + +| 模块 | 结论 | 可复用内容 | 新架构要求 | +| ---------------- | --------------- | ------------------------------------------------ | ------------------------------------------------------------------------ | +| Account 与 OAuth | 部分复用 | GitHub、Google、邮箱验证、Device Flow 和交互 | 采用 Account、AuthIdentity、Session;provider subject 绑定,不能只按邮箱 | +| Payment | 适配器级复用 | Stripe、EPay 签名、回调解析、金额单位转换 | 新建 ProviderEvent、PaymentAttempt、Order、Invoice、Refund | +| Plan | 规则思路复用 | 本地化名称、价格展示、feature limit、试用和审计 | Product entitlement 与 RuntimeClass 完全分离 | +| Credit 与 Usage | 账本思路复用 | before、after balance 和交易记录 | 改为 BillingAccount 或 Workspace,增加 reference 和 idempotency | +| Email | 高度复用 | SMTP、Resend、i18n template、HTML layout、幂等头 | 事务邮件进入持久队列,重写 Pod 相关模板 | +| Admin | UI 和查询可复用 | 用户、订单、收入、支出列表 | 显式平台角色、审计、限时 support impersonation | +| Lifecycle | 设计经验复用 | generation、fencing、retry、heartbeat、reconcile | 由 Cell Agent 和 DeploymentOperation 实现 | +| Marketplace | 独立保留 | 插件市场和公共站点 | 不承担数据面租户授权 | +| Pod 与 K8s | 废弃 | 无领域模型复用 | 使用 Cell、CloudInstance、Placement 和 DesiredState | + +### 13.2 必须废弃的旧模型 + +- AccountResources.MaxPods。 +- AccountUUID 到 namespace 的映射。 +- Pod 同时承载部署、租户、套餐和订阅状态。 +- 删除 Pod 时删除整个账户 namespace。 +- PaymentOrder 中的 PodUUID 和 PodSubdomain。 +- CloudPlan 同时包含售价、feature、image、CPU、memory 和 PVC。 +- Pod 到期、续费和购买任务作为 Subscription 真相。 +- Space Web API 直接操作 Kubernetes 资源。 + +这些模型与 Workspace 内嵌于 LangBot 实例的方向根本不同,不做兼容层。 + +### 13.3 推荐代码组织 + +- 保留 Space 作为 Marketplace、公共网站和 Cloud Portal/BFF。 +- 新建独立部署的 closed Cloud Control Plane。 +- 从 Space 提取 OAuth、Payment、Email 和 Admin 共享内部 Go package。 +- Control Plane 从第一天使用版本化 SQL migration,不依赖 AutoMigrate 作为生产 schema 变更机制。 +- Cell Agent 独立部署,只持有当前 Cell 所需的最小凭证。 + +## 14. Cloud v2 部署架构 + +### 14.1 拓扑 + +```mermaid +flowchart TD + Edge["Global Edge / Workspace Router"] --> CP["Cloud Control Plane"] + Edge --> CellA["Cell A"] + Edge --> CellB["Cell B"] + CP --> AgentA["Cell Agent A"] + CP --> AgentB["Cell Agent B"] + AgentA --> InstanceA["CloudInstance A"] + AgentA --> InstanceB["CloudInstance B"] + AgentB --> InstanceC["CloudInstance C"] + InstanceA --> WA["Workspace A / B / C"] + InstanceB --> WB["Workspace D"] + InstanceC --> WC["Workspace E / F"] +``` + +一个 Cell 提供共享基础设施: + +- Cell Agent 和 Cell ingress。 +- 计算资源池。 +- PostgreSQL cluster。 +- Valkey 或 Redis cluster。 +- Object Store 和 Secret Store 接入。 +- Box sandbox worker pool。 +- 日志、指标和审计出口。 + +每个 CloudInstance 在 Cell 内拥有独立的: + +- InstanceManifest 和 release。 +- Core deployment 与 service route。 +- PostgreSQL database 或 schema namespace。 +- cache、lock 和 secret prefix。 +- runtime ownership domain。 +- capacity budget。 + +物理 PostgreSQL、Valkey 和 Object Store 可以由同一个 Cell 共享,但 CloudInstance 的逻辑 namespace 不能共享。Router 的实际目标是 CloudInstance service,而不是一个无法区分实例的 Cell-wide Core API。 + +所有权矩阵: + +| 层级 | 拥有内容 | 不是它的职责 | +| ------------- | ---------------------------------------------------------------------- | ---------------------------------- | +| Cell | Agent、ingress、compute pool、存储集群、故障域和区域 | Workspace Membership、Subscription | +| CloudInstance | Core deployment、Manifest、DB namespace、release、runtime owner 和容量 | 全局账户、支付 | +| Workspace | 成员关系投影、业务资源、Plugin worker、Storage prefix 和 Entitlement | 物理集群、付款身份 | + +由于一个 CloudInstance 使用独立数据 namespace,Core 普通 Repository 在 namespace 内以 workspace_uuid 过滤;跨 namespace 的 Router、Storage、Cache 和运维协议必须同时使用 instance_uuid 与 workspace_uuid。 + +### 14.2 Placement + +workspace_placements 至少包含: + +```text +workspace_uuid +cell_uuid +cloud_instance_uuid +isolation_tier +region +residency +state +generation +desired_release +created_at +updated_at +``` + +Router 缓存 route_key 到 cell_uuid、cloud_instance_uuid、generation。路由切换必须 CAS generation,旧 generation 的内部请求和 runtime owner 必须被拒绝。 + +### 14.3 第一阶段运行约束 + +当前 Core manager 和平台连接是进程内单例。Cloud v2 MVP 应: + +- 每个 CloudInstance 的完整 Core 只运行一个 active replica。 +- 一个 runtime owner 可以服务多个 Workspace,但所有索引、连接和任务必须 Workspace-aware。 +- 只有 Edge、Portal 和 Control Plane 可以在此阶段独立无状态扩展。 +- 为每个 CloudInstance 设置 Workspace 和负载上限。 +- dedicated 套餐仍使用同一 Workspace 模型,只分配独享 CloudInstance 或 Cell。 + +当前 Core 尚未拆分 API 和 runtime,因此不能宣称 API replica 可独立扩展,也不能直接把现有整体 Deployment replicas 从 1 改为 2;否则平台长连接、scheduler、插件和消息发送可能重复执行。 + +### 14.4 后续 HA + +演进方向: + +- 将 API 与 runtime ownership 拆分。 +- runtime worker 按 Workspace 或 Bot 获取 lease。 +- lease 带 generation 和 fencing token。 +- scheduler、platform adapter 和 plugin worker 都受 lease 约束。 +- 旧 worker 恢复后不能继续发送消息或写入新 generation。 +- PostgreSQL 使用多 AZ 和 PITR。 +- Object Store 启用版本化。 +- Valkey 只做缓存、锁和临时状态,不作为权威数据库。 + +第一阶段不做单 Workspace 跨 Cell active-active。优先实现 Cell 内多 AZ 和跨 Cell 备份恢复。 + +### 14.5 Workspace 迁移 + +未来 Workspace 在 CloudInstance 间迁移的通用流程: + +1. Placement 进入 migrating。 +2. 目标实例建立 Manifest、Entitlement、directory projection 和数据副本。 +3. 通过 snapshot 与 delta 或 CDC 追平并记录 source watermark。 +4. 在源端开启 write fence,停止新写入并排空 API、job 和 outbox。 +5. 完成 final delta,校验 target watermark、资源计数和 checksum。 +6. 提升 placement generation;源数据库拒绝旧 generation 的后续写入。 +7. 冻结旧 runtime owner并停止旧平台连接。 +8. CAS 切换 route。 +9. 目标实例确认 projection、entitlement 和 runtime ready 后启动新 owner。 +10. 保留源端只读快照和回滚 generation 窗口。 + +该流程不依赖旧 Space Pod,也不要求 Workspace 等于 Deployment。 + +## 15. Core 服务层改造 + +### 15.1 WorkspaceScoped Repository + +所有业务 Service 显式接收 RequestContext: ```python -async def get_bots(self, ctx: RequestContext, include_secret: bool = True): +async def get_bot(self, ctx: RequestContext, bot_uuid: str) -> Bot: require(ctx, "bot.view") - query = sqlalchemy.select(Bot).where(Bot.workspace_uuid == ctx.workspace_uuid) + stmt = ( + select(Bot) + .where(Bot.workspace_uuid == ctx.workspace.workspace_uuid) + .where(Bot.uuid == bot_uuid) + ) + bot = await self.session.scalar(stmt) + if bot is None: + raise NotFoundError() + return bot ``` -需要改造的服务: +禁止: -- `UserService`:拆成 AccountService + WorkspaceService 更清晰。 -- `ApiKeyService`:按 workspace 管理 key。 -- `BotService`:所有 bot 查询/创建/更新/删除按 workspace。 -- `PipelineService`:所有 pipeline 查询/默认 pipeline 按 workspace。 -- `ModelProviderService` 和 model services:按 workspace 隔离 provider 和 model。 -- `MCPService`:按 workspace 管理 MCP server,运行时按 workspace host。 -- `KnowledgeService/RAGRuntimeService`:按 workspace 管理 KB、文件、collection。 -- `MonitoringService`:记录和查询都带 workspace。 -- `WebhookService`:按 workspace 管理 webhook。 -- `PluginRuntimeConnector`:插件安装、设置、配置按 workspace。 +- Service 内 select 全表后在 Controller 过滤。 +- 通过可选 workspace_uuid = None 表示系统权限。 +- 使用最近 Workspace 或默认 Workspace 执行后台任务。 +- 仅检查资源 uuid 而不检查 workspace_uuid。 -### HTTP API 形态 +需要逐一改造: -保留现有路径,靠 `X-Workspace-Id` 表示当前 workspace,可减少前端和 SDK 破坏: +- Account、Workspace、Membership、Invitation。 +- API Key。 +- Bot 和 Platform。 +- Pipeline 和 RunRecord。 +- ModelProvider 和所有 Model。 +- Plugin installation、configuration 和 page API。 +- MCP。 +- Knowledge 和 RAG。 +- Monitoring。 +- Webhook。 +- Binary Storage。 -- `GET /api/v1/workspaces` -- `POST /api/v1/workspaces` -- `GET /api/v1/workspaces/current` -- `PUT /api/v1/workspaces/current` -- `GET /api/v1/workspaces//members` -- `POST /api/v1/workspaces//invitations` -- `PUT /api/v1/workspaces//members/` -- `DELETE /api/v1/workspaces//members/` +### 15.2 后台任务 -现有资源 API: +后台任务 payload 必须包含: -- `/api/v1/platform/bots` -- `/api/v1/pipelines` -- `/api/v1/provider/*` -- `/api/v1/plugins` -- `/api/v1/mcp` -- `/api/v1/knowledge` +- instance_uuid。 +- workspace_uuid。 +- actor 或受约束的 SystemContext。 +- resource_uuid。 +- operation_id。 +- generation。 -继续保留,但必须从 `RequestContext.workspace_uuid` 过滤。 +跨所有 Workspace 的平台任务只能使用专门的 SystemContext,并由明确的 allowlist repository 执行,不能复用普通 Service 的 context=None。 -对外 API Key 也使用相同路径,只是由 key 决定 workspace。 +## 16. 运行时与 SDK 隔离 -### 初始化流程 +### 16.1 Query、Event 和 Session -现有 `/api/v1/user/init` 含义改为“创建首个账号和首个 workspace”: +Query 和 Event 增加 workspace_uuid 用于传递和观测,但它不是授权来源。Host 必须从可信 Bot、Pipeline、Plugin installation 或连接记录推导 Workspace,并校验传入字段一致。 -1. 如果系统没有任何 account: - - 创建 account。 - - 创建 personal/team workspace。 - - 创建 owner membership。 - - 创建默认 pipeline。 - - 标记 wizard status 到该 workspace metadata。 -2. 如果系统已有 account: - - 禁止无邀请注册,除非配置允许公开注册。 - - Space OAuth 登录后,如果没有 membership,引导创建 workspace 或接受邀请。 +Session key 至少为: -`/api/v1/user/account-info` 不应再只返回 first user,应返回: - -- `initialized` -- `registration_mode` -- `space_enabled` -- `default_login_methods` - -登录成功后前端调用 `/api/v1/workspaces` 选择 workspace。 - -### 运行时隔离 - -第一阶段采用共享进程 + workspace-aware runtime: - -- `RuntimeBot` 增加 `workspace_uuid`。 -- `RuntimePipeline` 增加 `workspace_uuid`。 -- `Query` 增加 `workspace_uuid`,从 bot/pipeline 派生。 -- `SessionManager.get_session()` key 从 `(launcher_type, launcher_id)` 改为 `(workspace_uuid, bot_uuid, launcher_type, launcher_id)`。 -- `PipelineManager.pipeline_dict` key 可保持 pipeline uuid,但所有 load/get 都校验 workspace;如果 uuid 不是全局唯一则改为 `(workspace_uuid, pipeline_uuid)`。 -- `ModelManager` provider/model 加 workspace 过滤;`get_model_by_uuid` 必须确保 query workspace 可访问。 -- `ToolManager` 中 MCP tools、plugin tools 按 query workspace 过滤。 - -后续规模化时可演进: - -- workspace runtime shard:每个 workspace 一套 plugin runtime/MCP runtime。 -- 大客户独立进程或独立数据库。 - -## Plugin SDK 与 Runtime 改造 - -### Query/Event 增加 workspace context - -SDK `Query` 增加: - -- `workspace_uuid: str` -- `workspace_slug: str | None` -- `account_uuid: str | None`,仅 Web/API 触发时可能有,聊天平台消息通常为空。 - -Event 模型通过 `event.query.workspace_uuid` 可拿到租户上下文;序列化时也应包含这些字段。 - -向后兼容: - -- 字段可选,默认 `None`。 -- 老插件不感知这些字段也能跑。 -- 新插件可通过 `ctx.event.query.workspace_uuid` 或新增 `ctx.get_workspace()` 访问。 - -### Host API 默认按当前 workspace 限制 - -`LangBotAPIProxy` 的以下方法必须由 Host 端按 workspace 过滤: - -- `get_bots` -- `get_bot_info` -- `send_message` -- `get_llm_models` -- `invoke_llm` -- `list_plugins_manifest` -- `list_commands` -- `list_tools` -- `call_tool` -- `invoke_embedding` -- `vector_*` -- `list_knowledge_bases` -- `retrieve_knowledge` - -建议新增显式方法: - -- `get_workspace_info()` -- `get_current_account()` -- `get_workspace_storage(...)` - -但不要让插件传入任意 workspace id 来越权访问。插件请求的 workspace 应由 Runtime 根据当前 query/plugin connection 填充。 - -### Workspace storage 修复 - -当前 SDK runtime 中: - -```python -data["owner_type"] = "workspace" -data["owner"] = "default" +```text +workspace_uuid +bot_uuid +launcher_type +launcher_id ``` -必须改为: +Query、Session、RuntimeBot、RuntimePipeline、cache key、lock key 和 manager index 都必须包含 Workspace。 -- 如果请求来自 query/event:owner 为 `workspace_uuid`。 -- 如果请求来自后台插件任务:owner 为 plugin 安装所属 workspace。 -- Host 侧 `binary_storages` 加 `workspace_uuid`,并在 unique key 中包含 workspace。 +### 16.2 Plugin -Plugin storage 建议也同时加 workspace: +SaaS 不允许多个 Workspace 共享同一个第三方插件进程或可信 Host 连接。原因是插件可以保留跨请求内存、启动后台任务并主动调用 Host API。 -- 现在 plugin storage owner 是 `author/name`,这会导致同一插件在不同 workspace 的私有数据冲突。 -- 应改为 `(workspace_uuid, plugin_id, key)`。 +安全不变量: -### 插件安装与配置 +- 一个不可信 Plugin 进程、容器或 supervisor 不能服务多个 Workspace。 +- Plugin code artifact 和只读包缓存可以共享。 +- Plugin config、secret、storage 和 connection 不能跨 Workspace 共享。 +- Host connection 在服务端至少绑定 workspace_uuid;每次调用还绑定不可伪造的 installation capability。 +- Plugin 不能提交任意 workspace_uuid。 +- Host API 从连接上下文推导 Workspace。 +- Plugin page API 同时校验 Account Membership 和 Plugin installation。 -`plugin_settings` 从全局变为 workspace-scoped: +隔离等级: -- 同一个插件可安装到多个 workspace。 -- 每个 workspace 有自己的 enabled、priority、config、install_source、install_info。 -- 插件 runtime 列表需要能按 workspace 过滤。 +| Level | 进程边界 | 用途 | +| -------------------- | ---------------------------------------------------------------------- | ------------------------------------- | +| workspace_worker | 一个 Workspace 一个 supervisor,可承载该 Workspace 内多个 installation | SaaS 默认基线 | +| installation_sandbox | 每个 installation 独立进程或容器 | 高风险、未知来源或申请高权限的 Plugin | +| dedicated_worker | Workspace 独享节点或更强 sandbox | 企业与高隔离套餐 | -实现路线有两种: +同一 Workspace 内多个 Plugin 共享 worker 意味着它们共享租户内信任域;一旦需要防止 Plugin 之间读取内存或 secret,必须提升为 installation_sandbox。具体分级由签名、来源、权限声明和产品策略决定,MVP 不必无条件为每个 installation 启动容器。 -1. 共享插件进程,插件代码只加载一份,设置和调用时附带 workspace。 -2. 每个 workspace 一个插件容器实例,隔离最彻底但资源占用更高。 +OSS 因为实例只有一个 Workspace,可以继续使用单一插件 runtime,但协议和存储仍必须 Workspace-aware。 -推荐第一阶段采用方案 1,但要求: +### 16.3 Plugin Host API -- 所有 RuntimeToLangBot/PluginToRuntime action 都能携带 `workspace_uuid`。 -- 插件 config 获取时按 workspace 返回。 -- 插件 page API 请求必须校验当前用户在该 workspace 有访问权限。 +以下 API 必须限定当前 Workspace: -### MCP +- get_bots 和 get_bot_info。 +- send_message。 +- get_llm_models 和 invoke_llm。 +- list_plugins_manifest。 +- list_commands 和 list_tools。 +- call_tool。 +- invoke_embedding 和 vector 相关接口。 +- list_knowledge_bases 和 retrieve_knowledge。 +- workspace storage。 -MCP server 是租户资源: +跨 Workspace 管理能力不能通过普通 Plugin Host API 暴露。 -- `mcp_servers.workspace_uuid`。 -- MCP session key 从 `server_name` 改为 `(workspace_uuid, server_name)` 或使用全局 uuid。 -- Pipeline extension preferences 中绑定 MCP server uuid 时,只能绑定同 workspace 的 server。 -- MCP tool 列表在 query 执行时按 query.workspace_uuid + pipeline 绑定关系过滤。 +### 16.4 MCP -## 前端改造 +- mcp_servers 带 workspace_uuid。 +- MCP runtime key 使用 workspace_uuid 与 server_uuid。 +- 同名 server 或 tool 不能共享 session。 +- Pipeline 只能引用同 Workspace 的 MCP server。 +- 当前全局 mcp-shared Box session 必须改成每 Workspace 隔离。 +- MCP Resource 的 list、read、附件和二进制读取必须在 Workspace 内校验。 -### Workspace selector +### 16.5 Box -Home layout 顶部或 sidebar 增加 workspace selector: +Box namespace 至少是 instance_uuid 与 workspace_uuid: -- 当前 workspace 名称和头像。 -- 切换 workspace 后写入 `localStorage.currentWorkspaceId`。 -- 所有请求自动带 `X-Workspace-Id`。 -- 切换后刷新 sidebar 数据和页面缓存。 +- Session、Process、目录、端口和 WebSocket attach token 都在该 namespace 内。 +- SaaS 插件不能传任意 host_path。 +- 不允许额外特权挂载。 +- 所谓 global sandbox 只能表示 Workspace 内 global,不能表示整个 CloudInstance global。 +- 配额从 EntitlementSnapshot 获取并在 Core 或 Box gateway 执行。 -`BaseHttpClient` request interceptor 增加: +### 16.6 RAG、Vector 和 Object Storage -```ts -const workspaceId = localStorage.getItem("currentWorkspaceId"); -if (workspaceId) config.headers["X-Workspace-Id"] = workspaceId; +- collection 的物理名称由服务端使用 instance_uuid 和 workspace_uuid 派生。 +- Plugin 只使用不透明 collection handle。 +- 所有 embedding 和 retrieval 记录带 workspace_uuid。 +- 对象路径固定在 instances、instance_uuid、workspaces、workspace_uuid 前缀下。 +- 外部 API 不接受任意 storage path。 +- Secret Store 和 Redis key 必须带 Workspace。 +- 日志和 trace 保留完整 instance_uuid 与 workspace_uuid。 +- Metrics 使用 Cell、plan、runtime class 等受控低基数标签;除受控小规模内部指标外,不把完整 Workspace UUID 作为时序标签,避免高基数爆炸。 + +## 17. HTTP API 与前端 + +### 17.1 OSS Core Workspace API + +OSS Core 提供本地 Workspace 和成员协作 API: + +```text +GET /api/v1/workspaces +POST /api/v1/workspaces +GET /api/v1/workspaces/current +GET /api/v1/workspaces/{workspace_uuid} +GET /api/v1/workspaces/{workspace_uuid}/members +POST /api/v1/workspaces/{workspace_uuid}/invitations +POST /api/v1/invitations/accept +PATCH /api/v1/workspaces/{workspace_uuid}/members/{account_uuid} +DELETE /api/v1/workspaces/{workspace_uuid}/members/{account_uuid} ``` -### 用户与成员管理页面 +GET 返回实例唯一 Workspace;POST /workspaces 为保持客户端契约可以存在,但固定返回 edition_limit。Invitation token 放在 POST body 中并全链路日志脱敏,不能放在 URL path、query、Referer 或分析事件中。Invitation 接受和 Membership 修改在 OSS 本地事务中完成。 -新增页面: +### 17.2 SaaS Cloud API -- `/home/workspace/settings` -- `/home/workspace/members` -- `/home/workspace/invitations` +SaaS 的全局 Workspace lifecycle、Membership 和 Invitation 权威写入由 Control Plane 或 Cloud BFF 提供: -能力: +```text +GET /cloud/v1/workspaces +POST /cloud/v1/workspaces +GET /cloud/v1/workspaces/{workspace_uuid} +GET /cloud/v1/workspaces/{workspace_uuid}/members +POST /cloud/v1/workspaces/{workspace_uuid}/invitations +POST /cloud/v1/invitations/accept +PATCH /cloud/v1/workspaces/{workspace_uuid}/members/{account_uuid} +DELETE /cloud/v1/workspaces/{workspace_uuid}/members/{account_uuid} +GET /cloud/v1/workspaces/{workspace_uuid}/billing +``` -- owner/admin 邀请成员。 -- owner/admin 修改成员角色。 -- owner 移除成员、转让 owner。 -- 所有人可切换 workspace。 -- viewer/operator 在 UI 上隐藏不可操作按钮,但后端仍做权限校验。 +OSS endpoint 由 LocalDirectoryProvider 执行;Cloud endpoint 由 Control Plane directory service 执行。Cloud API 完成事务后发布 DirectoryEvent,并在需要立即进入 Workspace 的流程中等待目标 Core 的 contiguous_applied_sequence 达到 required instance_sequence。SaaS 不应把 Invitation 或 Membership 写入请求直接发送给 Data Plane。 -### 登录与注册 +SaaS Core 对外只提供当前 placement 内的 Workspace 投影读取和业务资源 API。对用户发起的 directory mutation 固定返回 cloud_directory_read_only。控制面投影写入使用独立的 mTLS internal endpoint 或 event consumer,不复用用户管理 API。 -登录后流程: +现有 Bot、Pipeline、Provider、Plugin、MCP、Knowledge 和 Monitoring API 路径可保留,但必须从 RequestContext 限定 Workspace。 -1. `authUser` 拿 token。 -2. `initializeUserInfo()` 获取 account info。 -3. `GET /api/v1/workspaces`。 -4. 如果没有 workspace:进入创建 workspace 向导。 -5. 如果有多个 workspace:默认进入最近使用 workspace,可切换。 +### 17.3 OSS 前端 -注册页不再表达“初始化管理员账号”,而是: +- 不显示 Workspace switcher 和 Create Workspace。 +- 可以显示唯一 Workspace 名称。 +- 提供 Members 和 Invitations 页面。 +- 提供固定角色选择。 +- 初始化页面创建首位 owner 和唯一 Workspace。 -- 首次系统启动:创建首个 owner + default workspace。 -- 后续:根据配置允许公开注册,或只能接受邀请。 +### 17.4 SaaS 前端 -### 旧页面影响 +- Cloud shell 从 Control Plane 获取全局 Workspace directory。 +- 明确选择 Workspace 后进入稳定 Workspace route。 +- 显示 Workspace switcher、Create Workspace、Members、Billing 和 Usage。 +- 切换 Workspace 后清理前一个 Workspace 的客户端 query cache。 +- 所有 Data Plane 请求显式带当前 Workspace 选择器。 +- 不能仅把 currentWorkspaceId 写入 localStorage 就认为完成安全隔离。 -需要逐个检查这些页面的数据加载是否都依赖当前 workspace: +### 17.5 登录后的 SaaS 路径 -- Bots -- Pipelines -- Plugins/Market/MCP -- Knowledge -- Monitoring -- Models dialog -- API integration dialog -- Wizard +1. Control Plane 完成全局登录。 +2. 前端读取 Workspace directory。 +3. 用户选择 Workspace,或注册工作流明确返回刚创建的 Workspace。 +4. Router 解析 Placement。 +5. Edge 验证 Account token并签发 instance-scoped assertion;Data Plane 验证 assertion、Workspace、Placement generation 和本地 Membership 投影。 +6. 前端进入 Workspace 内页面。 -## 迁移方案 +普通资源请求缺少 Workspace 时不做隐式跳转,由前端处理 workspace_required。 -### 迁移阶段 0:准备 +## 18. 故障与降级策略 -- 引入 `workspaces`、`workspace_memberships`、`workspace_invitations`。 -- 给 `users` 增加 `uuid/status/display_name` 等字段。 -- 创建 `RequestContext`,但先不强制所有服务改完。 +### 18.1 不同租约不能混用 -### 迁移阶段 1:默认 workspace +Manifest、Account token、Directory freshness 和 Entitlement 使用不同的有效期: -对现有实例执行迁移: +| 状态 | 建议有效期 | 控制面故障后的行为 | +| ------------------------- | ---------------------------- | ---------------------------------------------------------------- | +| InstanceManifest | 长于最大 entitlement grace | 运行到 exp;过期后整个 SaaS instance fail closed | +| Account access token | 分钟级短 TTL | 已签发 token 只运行到 exp;不能登录或刷新 | +| InteractiveDirectoryLease | 分钟级,独立可配置 | 交互用户访问只运行到 lease 到期,之后 fail closed | +| WorkspaceStatusLease | 可长于交互 lease,但有硬上限 | Bot、Webhook、API Key 只运行到 lease 到期,之后安全暂停 | +| Entitlement grace | 小时到天 | 只维持已有自动化工作负载和已购能力,不延长 Manifest 或目录 lease | +| Router cache | 短 TTL,带 generation | 可路由到最后已知健康目标;不能忽略 generation | -1. 创建 `Default Workspace`。 -2. 找到现有第一个 user,设为 owner。 -3. 所有已有资源写入 `workspace_uuid=default_workspace_uuid`。 -4. `metadata` 迁入 default workspace;确实全局的配置放到 `system_metadata`。 -5. `binary_storages` 中 `owner_type=workspace, owner=default` 改为 owner 为 default workspace uuid。 -6. 插件 `plugin_settings` 归入 default workspace。 +因此“24 到 72 小时宽限”只适用于 Entitlement 和已有 Bot 等自动化工作负载,不代表用户可以在身份服务或 Directory 断开后继续登录 24 到 72 小时。 -### 迁移阶段 2:服务层强制 scope +### 18.2 故障矩阵 -- 改所有 service 查询,必须要求 `workspace_uuid`。 -- API Key 迁移为 workspace key。 -- 所有写操作必须检查权限。 -- 监控和任务查询按 workspace 过滤。 +| 故障 | 交互用户 | 已有 Bot、Webhook、Workspace API Key | 控制类操作 | +| ------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------- | ------------------------------------ | +| Manifest signer 或续签不可用 | 有效 Manifest 与 token 内可用 | 只运行到 Manifest exp | 新实例绑定禁止 | +| Identity 不可用 | 已有 token 到期前可用,不能登录或刷新 | 不受 Account token 影响,但仍检查 WorkspaceStatusLease | 禁止 | +| Directory 不可用 | 有效 token 与 InteractiveDirectoryLease 内可用 | 只运行到 WorkspaceStatusLease exp | 邀请、角色、Workspace lifecycle 禁止 | +| Billing 或 Entitlement signer 不可用 | 按最后快照读取已有资源 | 同时满足有效 Manifest、WorkspaceStatusLease 和 grace 才能继续 | 购买、提额、套餐变更禁止 | +| Router control 不可用 | 使用未过期 route cache | 已建立的数据面连接仍检查 generation 和所有 lease | 新 Placement 和迁移禁止 | +| Data Plane 不可用 | 当前 Workspace 不可用 | 当前 Workspace 停止 | Control Plane 只显示故障,不伪造成功 | -### 迁移阶段 3:运行时上下文 +UsageEvent 在控制面故障时写入 durable outbox。 -- `Query`、`Session`、`RuntimeBot`、`RuntimePipeline` 增加 workspace。 -- Plugin/MCP/Model/RAG runtime 全部按 workspace 过滤。 -- 修复 SDK workspace storage。 +### 18.3 必须 fail closed 的情况 -### 迁移阶段 4:前端多 workspace +- SaaS 首次启动没有有效 InstanceManifest。 +- Manifest 或 Entitlement 签名无效。 +- audience 或 instance_uuid 不匹配。 +- revision 或 placement generation 回滚。 +- 已知 Workspace suspended。 +- 没有本地 Membership 投影。 +- InteractiveDirectoryLease 已过期的交互请求。 +- WorkspaceStatusLease 已过期的 Bot、Webhook、API Key、job 或 runtime 请求。 +- SaaS 请求缺少 Workspace。 -- 登录后 workspace 选择。 -- Header/sidebar workspace switcher。 -- 成员和邀请管理。 -- 所有 API 请求带 `X-Workspace-Id`。 +不得回退到 OSS mode、default Workspace 或全局资源访问。 -### 迁移阶段 5:安全收敛 +### 18.4 Membership 撤销与 Workspace 暂停 -- 添加跨 workspace 越权测试。 -- 添加 API Key scope 测试。 -- 添加插件 Host API 过滤测试。 -- 添加 MCP 和 RAG 隔离测试。 +- Account token 使用短 TTL。 +- Membership 变更立即发布高优先级 DirectoryEvent。 +- 本地 projection 保存 membership_revision。 +- Edge 或 Cloud BFF 的签名内部 assertion 携带 directory_min_sequence;Account access token 本身不携带 Membership revision。 +- Core 的 contiguous_applied_sequence 低于 directory_min_sequence 时拒绝请求并等待同步,不能按旧 Membership 放行。 +- InteractiveDirectoryLease 设置用户撤权最坏生效上限;WorkspaceStatusLease 设置安全暂停对自动化工作负载的最坏生效上限。 +- Cell 无法续租时,交互用户、Bot、Webhook、API Key、job 和 runtime 分别在对应 lease 到期后 fail closed。 +- 安全暂停来自 Directory revision,欠费暂停来自 Entitlement revision;两者任一个禁止时都不能运行受影响操作。 +- Billing entitlement grace 不能延长或覆盖安全暂停和 WorkspaceStatusLease。 +- 紧急暂停的最坏生效时间由 WorkspaceStatusLease TTL 明确写入安全 SLA。 -## 安全边界 +### 18.5 Entitlement 到期 -必须防的场景: +宽限期结束后: -- 用户 A 修改 URL 中 bot uuid,访问用户 B workspace 的 bot。 -- API Key 来自 workspace A,但调用 workspace B 的 bot。 -- 插件通过 `get_bots()` 枚举所有 workspace 的 bot。 -- 插件通过 `workspace_storage` 读取其它 workspace 的数据。 -- MCP server 名称相同导致 session 复用。 -- monitoring session_id 相同导致数据串租户。 -- Space OAuth 登录时,同 email 账户被错误绑定到已有本地 account。 +- 阻止付费功能的新建和扩大资源。 +- 是否停止已有 Bot 应作为独立产品策略,不能由通用 quota 代码隐式决定。 +- 欠费暂停必须可审计并支持恢复。 -建议策略: +## 19. 安全清单 -- 所有资源访问都使用 `workspace_uuid + resource_id`。 -- 所有 service 方法入口做权限检查。 -- 插件 Host API 的 workspace 不信任插件入参,只信任 query/runtime connection 上下文。 -- API Key 只授予最小 scope,默认不允许成员管理。 -- owner 角色不能被普通 admin 移除或降权。 +必须防止: -## 实施优先级 +- Account A 猜测 Account B Workspace 的资源 uuid。 +- API Key A 通过 header 选择 Workspace B。 +- Plugin 枚举其他 Workspace 的 Bot、Model 或 Knowledge。 +- Plugin 在内存中保留 Workspace A 数据后服务 Workspace B。 +- MCP 同名 server 复用 session。 +- Box 同名 session、process 或 attach token 串租户。 +- RAG collection name 冲突。 +- Monitoring session_id 冲突。 +- 后台任务缺少 Workspace 后访问全表。 +- OAuth 只按 email 错误合并不同 provider identity。 +- 邀请并发消费或移除最后一个 owner。 +- 伪造 X-Workspace-Id、内部 assertion 或旧 placement generation。 -### P0:基础租户骨架 +安全原则: -- Account uuid/status。 -- Workspace / Membership / Invitation。 -- RequestContext。 -- JWT 改为 account uuid。 -- 前端 current workspace header。 +- 最小权限。 +- 服务端推导 Workspace。 +- 复合外键。 +- 密钥只保存 hash。 +- 内部通信使用 mTLS 和短期签名 service token。 +- 支付 webhook 先持久化再履约。 +- 所有控制面事件和运行操作幂等。 +- 跨租户访问默认返回 404。 -### P1:资源行级隔离 +## 20. 实施阶段 -- Bots、Pipelines、Models、MCP、Plugins、Knowledge、Monitoring、API Keys 全部加 workspace_uuid。 -- service 查询统一加 workspace filter。 -- 权限矩阵落地。 +### Phase 0:契约和基线 -### P2:运行时隔离 +- 冻结术语、产品矩阵和 Source of Truth。 +- 定义 RequestContext、ExecutionContext 和 Provider 接口。 +- 定义 DirectoryEvent、DirectoryLease、InstanceManifest、EntitlementSnapshot、UsageEvent 和 Placement generation schema。 +- 建立跨租户测试辅助工具。 +- 列出所有需要 workspace_uuid 的表、cache、lock、storage 和 runtime key。 -- Query、Session、RuntimeBot、RuntimePipeline 加 workspace。 -- Plugin Host API 和 MCP tools 按 workspace 过滤。 -- SDK workspace storage 从 `default` 改为真实 workspace。 +### Phase 1:OSS tenancy kernel -### P3:协作体验 +- Account 引入稳定 uuid。 +- 新增 Workspace、Membership、Invitation。 +- 首次初始化创建唯一 Workspace 和 owner。 +- 后续邀请注册加入唯一 Workspace。 +- 固定 RBAC。 +- OSS edition policy 强制 workspace_limit = 1。 +- JWT 从 email 主体迁移到 account_uuid。 -- 邀请成员。 -- 成员列表和角色管理。 -- workspace switcher。 -- 最近使用 workspace。 +### Phase 2:资源行级隔离 -### P4:SaaS 运维增强 +- 按 expand、backfill、validate、contract 迁移所有现有数据到默认 Workspace。 +- 先增加可回填字段,再校验所有行和关联,最后收紧非空与复合外键。 +- Repository 和 Service 强制 scope。 +- API Key、Webhook 和 Monitoring 完成隔离。 +- SQLite 和 PostgreSQL 迁移均可中断重试并支持 forward recovery。 -- Workspace 级用量统计。 -- Workspace 级限额:max_bots/max_pipelines/max_extensions/tokens/storage。 -- 审计日志。 -- workspace suspend/delete。 -- 可选自定义角色。 +SQLite 增加复合外键或非空字段可能需要重建表。升级前必须备份并验证恢复,不承诺任意阶段无损 downgrade。 -## 测试计划 +### Phase 3:Runtime 和 SDK 隔离 -后端测试: +- Query、Event、Session 和 ExecutionContext 增加 Workspace。 +- Bot、Pipeline、Model、RAG manager 的 key 和 cache 改造。 +- Plugin connection、process、config 和 storage 隔离。 +- MCP 和 Box 隔离。 +- SDK Host API 不允许任意 Workspace 参数。 -- 账户可加入多个 workspace。 -- 同账户在不同 workspace 权限不同。 -- viewer 不能创建/修改资源。 -- API Key 只能访问所属 workspace。 -- 所有资源 list/get/update/delete 都不能跨 workspace。 -- 默认 workspace 迁移后旧数据可用。 +### Phase 4:Cloud 协议 -运行时测试: +- InstanceManifest 和 JWKS。 +- SaaS Account、Workspace、Membership projection。 +- Directory inbox/outbox。 +- instance_sequence、snapshot reconciliation、InteractiveDirectoryLease 和 WorkspaceStatusLease。 +- Entitlement cache 和 quota enforcement。 +- Usage outbox。 -- 两个 workspace 使用相同 `launcher_id` 不共享 session。 -- 两个 workspace 使用相同 MCP server name 不共享 MCP session。 -- 插件 `get_bots()` 只能看到当前 workspace bot。 -- 插件 `workspace_storage` 在不同 workspace 读写隔离。 -- Pipeline 只调用当前 workspace 绑定的插件和 MCP tools。 +### Phase 5:Closed Control Plane 与 Billing -前端测试: +- 全局 Account 和 Workspace directory。 +- BillingAccount、Product、Plan、Price 和 Subscription。 +- Payment provider event 和财务模型。 +- Entitlement signer 和 Usage ledger。 +- Cloud Portal 和运营后台。 -- 登录后自动进入最近 workspace。 -- 切换 workspace 后列表数据变化。 -- 无权限按钮隐藏,直接调用 API 也被后端拒绝。 -- 邀请成员流程完整。 +### Phase 6:Cloud Cell -迁移测试: +- Cell Agent。 +- CloudInstance、Placement 和 Router。 +- WorkspaceExecutionState、签名 gateway assertion 和 Repository write fence。 +- Workspace provisioning workflow。 +- runtime ownership 和 generation fencing。 +- shared 与 dedicated placement。 +- Cloud v2 新用户完整链路。 -- SQLite 老实例迁移。 -- PostgreSQL 老实例迁移。 -- 已有 local account 迁移为 default workspace owner。 -- 已有 Space account token 和 Space model provider API key 不丢失。 +### Phase 7:HA 与规模化 -## 关键实现注意事项 +- API 与 runtime worker 拆分。 +- Workspace 或 Bot lease。 +- 同 Cell 多 AZ。 +- Workspace migration。 +- 跨 Cell DR。 -- 不建议在第一版做数据库 schema-per-tenant。LangBot 当前 ORM 和运行时均以单库单表为主,先做 shared schema + workspace_uuid 成本更低。 -- 不建议每个 workspace 立即启动独立 plugin runtime。先共享 runtime,强制 action 带 workspace;大客户隔离可作为后续部署形态。 -- 不要只在前端过滤 workspace。插件、API Key、MCP、RAG 都能绕过前端,必须在后端和运行时层过滤。 -- `metadata` 要拆清楚:wizard status 属于 workspace,系统版本/迁移信息属于 system。 -- `users.user` 用 email 当主键语义不稳,应尽快引入 `account_uuid` 并让 JWT 以 uuid 为准。 -- `plugin_settings` 当前主键没有 workspace,改造时要先改主键/唯一约束,否则同插件无法在多个 workspace 配不同配置。 +### Multi-workspace 发布门禁 -## 建议落地顺序 +- Phase 1 和 Phase 2 只允许发布 OSS 单 Workspace 多用户。 +- 数据库出现 workspace_uuid 不代表可以启用 SaaS 多 Workspace。 +- 在 Phase 3 完成前,Plugin、Session、MCP、Box 和 Runtime 仍可能有全局状态,任何环境都不得创建第二个生产 Workspace。 +- SaaS multi_workspace capability 只有在资源隔离、Runtime、SDK、Directory projection、两类 DirectoryLease、Manifest、Entitlement、Placement write fence 和跨租户测试全部通过后才能由签名 Manifest 开启。 +- Manifest 应携带 tenant_isolation_version;Core 只接受自身声明支持且通过 release gate 的版本。 +- 未通过门禁的 release 固定执行 workspace_limit = 1,即使 Control Plane 错误下发更高额度也不放开。 +- 第一批 schema PR 不暴露 SaaS Workspace 创建、切换或 Cloud directory 写入。 -1. 新增 workspace/account/membership 表和 RequestContext。 -2. 迁移旧数据到 default workspace。 -3. 改 auth 和前端请求头,让每个请求都有 current workspace。 -4. 从最核心资源开始逐个加 scope:bot -> pipeline -> provider/model -> plugin/MCP -> knowledge -> monitoring。 -5. 改 SDK Query/Event 和 runtime storage。 -6. 上成员管理 UI 和邀请。 -7. 做越权测试和迁移测试。 +## 21. 第一批代码改动建议 -这个顺序的好处是可以较早让主 UI 在一个 workspace 下继续工作,同时把最危险的跨租户泄露面逐步收紧。 +第一批 PR 只做可独立验证的基础骨架,不同时改完所有资源: + +1. 新增 Account uuid、Workspace、Membership、Invitation 表和 Alembic migration。 +2. 迁移旧实例到唯一 Default Workspace。 +3. 首个用户成为 owner。 +4. 引入 RequestContext 和固定权限定义。 +5. JWT sub 改为 account_uuid,并保留兼容读取旧 token 的短期迁移策略。 +6. OSS 创建第二个 Workspace 返回 edition_limit。 +7. 增加 OSS 多用户邀请的 Service 测试。 +8. 增加两个 Workspace 的 Repository 隔离测试基础设施。 + +第一批 PR 不开启 SaaS multi_workspace capability。第二批再从 Bot 开始逐资源强制 Workspace scope,不能长期停留在“表已有 workspace_uuid、查询仍访问全表”的中间状态。 + +## 22. 测试与验收 + +### 22.1 Migration + +- SQLite 旧实例升级。 +- PostgreSQL 旧实例升级。 +- 已有 Account 成为默认 Workspace owner。 +- 所有旧资源完成 workspace_uuid backfill。 +- migration 中断后可安全重试。 +- SQLite 重建表前生成并验证备份,失败时可 forward recover。 +- system metadata 与 workspace metadata 正确拆分。 + +### 22.2 OSS 产品行为 + +- 首次注册原子创建 Account、唯一 Workspace 和 owner。 +- 第二个 Workspace 无法创建。 +- 邀请用户可以注册并加入唯一 Workspace。 +- 未配置 SMTP 时一次性邀请链接仍可完成注册。 +- viewer、operator、developer、admin 和 owner 权限符合矩阵。 +- 最后一个 owner 不能被移除或降级。 +- OSS 不显示 Workspace switcher,但显示成员管理。 + +### 22.3 SaaS 注册与目录 + +- 普通注册只创建一个 personal Workspace。 +- 邀请注册创建 personal Workspace 并加入目标 Workspace。 +- 已有用户接受邀请不创建额外 personal Workspace。 +- 未 active Placement 的 provisioning 或 dormant Workspace 不能创建业务资源,首次使用完成 Placement 后才可进入。 +- 重试注册工作流不产生重复数据。 +- DirectoryEvent 重复、乱序和延迟时投影正确。 +- snapshot 与增量 cursor 能恢复断流,instance_sequence 缺口不会被错误标记 ready。 +- contiguous_applied_sequence 未达到 lease.min_required_sequence 时请求 fail closed。 +- SaaS pending Invitation、email 和 token hash 不进入 Data Plane。 +- 缺少 Workspace 的 Data Plane 请求返回 workspace_required。 + +### 22.4 资源隔离 + +即使 OSS UI 只允许一个 Workspace,CI 也必须使用测试 policy 创建至少两个 Workspace,覆盖每类资源的: + +- list。 +- get。 +- create。 +- update。 +- delete。 +- export。 +- 关联与复制。 + +还要覆盖 Pipeline 引用其他 Workspace 的 Model、Knowledge、MCP 或 Plugin 时被拒绝。 + +### 22.5 Runtime + +- 两个 Workspace 使用相同 launcher_id 不共享 Session。 +- 相同 MCP server name 不共享连接或 tool。 +- Plugin 猜测 Query ID 或 resource uuid 无法越权。 +- Plugin 不能伪造 workspace_uuid。 +- 同一 Plugin 在两个 Workspace 使用独立 process、connection、config 和 storage。 +- Box 同名 Session、Process 和 WebSocket attach 不串租户。 +- RAG collection 和对象路径不串租户。 +- Runtime lease 过期或 generation 变化后旧 owner 无法继续发送消息。 +- 普通 API、Plugin Host、job 和 runtime 携带旧 placement_generation 时都不能写入。 +- 并发写事务与 fence 使用 shared/exclusive lock,不会发生校验后 fence、旧事务再提交的竞态。 +- 旧 generation outbox event、Object Store staging write 和平台副作用在 dispatcher 再校验时被拒绝。 + +### 22.6 Billing 与控制面 + +- Payment webhook 重复投递只履约一次。 +- 订单成功不会直接绕过 Subscription 状态机。 +- Entitlement 签名、过期、回滚和 audience 校验。 +- Manifest root trust、delegated JWKS、续签、过期和 key revocation。 +- InteractiveDirectoryLease 到期后交互用户停止,WorkspaceStatusLease 到期后自动化工作负载停止。 +- UsageEvent 重复和乱序不会重复计费。 +- 迟到 UsageEvent 由中央 ledger 按 occurred_at 和不可变 Subscription/Price version 归入正确账期。 +- past_due、grace、suspended 支付恢复和 canceled_at_period_end 撤销会生成正确的新 Entitlement revision。 +- Control Plane 断开时按宽限策略运行。 +- 没有有效 Manifest 时 SaaS fail closed。 +- Placement 切换后旧 generation 请求被拒绝。 + +### 22.7 端到端 + +- OSS 初始化、邀请、注册、登录、成员管理和权限拒绝。 +- SaaS 注册、personal Workspace、创建 Workspace、切换、邀请和计费。 +- Workspace A 与 B 的 Bot、Pipeline、Plugin、MCP、Knowledge 和 Monitoring 完整隔离。 +- Cloud Router 到正确 Cell 和 CloudInstance。 +- 欠费、暂停、恢复和 entitlement 更新。 + +端到端验收必须使用真实浏览器操作,不以组件测试或接口调用替代用户路径。 + +## 23. 已确认决策与待定项 + +### 23.1 已确认 + +- Workspace 是 LangBot 实例级逻辑租户。 +- OSS 单 Workspace、多用户。 +- SaaS 多 Workspace。 +- SaaS 注册自动创建 personal Workspace。 +- Cloud 部署绿地重做,不兼容旧 Pod 架构。 +- Core 内置完整 Workspace 隔离。 +- 使用独立闭源 Control Plane 管理 SaaS 商业和 Cloud 能力。 +- Workspace 与 Deployment 分离。 +- Product entitlement 与 runtime capacity 分离。 + +### 23.2 推荐默认值 + +- OSS 初始化后默认仅邀请注册。 +- OSS 使用固定角色。 +- SaaS Membership 权威在 Control Plane,Core 使用本地版本化投影。 +- Subscription 以 Workspace 为 subject,BillingAccount 作为付款主体。 +- personal Workspace 使用免费 entitlement。 +- SaaS Plugin installation 按 Workspace 隔离进程。 +- entitlement grace 为 24 到 72 小时。 +- Cloud v2 MVP 每个 CloudInstance 一个 active runtime owner。 + +### 23.3 后续需要产品确认 + +- personal Workspace 是否计入可拥有 Workspace 上限。 +- personal Workspace 是否允许邀请成员。 +- 订阅按 Workspace 独立购买,还是企业 BillingAccount 统一承诺消费。 +- past_due 宽限期后是否停止已有 Bot,还是只禁止新建和扩容。 +- 各计划的成员、Bot、Token、Storage 和 Plugin 限额。 +- 企业私有化是否需要离线 Commercial Module。 +- 自定义角色、SSO、SCIM 和高级审计的套餐边界。 + +## 24. 最终结论 + +LangBot 多租户不能等同于把旧 Space Pod 变成共享 Pod,也不能只把 Workspace UI 放进闭源模块。 + +正确的边界是: + +- Core 开源并负责安全:单 Workspace 多用户、固定 RBAC、数据和运行时隔离。 +- Control Plane 闭源并负责 SaaS:全局 Account、多 Workspace 编排、Subscription、Billing、Entitlement、Placement 和 Cloud 运维。 +- Space 复用外围能力:OAuth、支付适配器、邮件、后台和可靠性经验。 +- Cloud v2 重新设计:一个 CloudInstance 承载多个 Workspace,shared 或 dedicated 只是 placement 策略。 + +这样既能让 OSS 成为完整、安全的多人协作产品,也能把真正的 SaaS 商业能力和全局控制能力留在独立闭源服务中。