mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-19 19:06:07 +00:00
1757 lines
80 KiB
Markdown
1757 lines
80 KiB
Markdown
# LangBot Workspace 多用户与 SaaS 多租户架构
|
||
|
||
> **架构修订中:** 已确认的 SaaS 形态是一个逻辑 LangBot 实例承载全部 Workspace;当前不做分布式,
|
||
> 但协议和状态必须支持未来在同一逻辑实例内增加 replica、worker 和数据库 shard。
|
||
> 本文后续 Cell、CloudInstance、Workspace Placement 和按 CloudInstance 分配数据库/Runtime 的章节属于旧提案,
|
||
> 在 [待决策项](./pending-architecture-decisions.md) 完成讨论前不得作为最终 Cloud v2 拓扑实施。
|
||
|
||
## 1. 决策摘要
|
||
|
||
本方案采用以下架构:
|
||
|
||
> 开源 LangBot Core 提供完整的租户隔离内核和单 Workspace 多用户能力;SaaS 使用独立闭源 Cloud Control Plane 管理全局账户、多 Workspace、计费、权益和 SaaS 生命周期。
|
||
|
||
这是本次设计的核心边界:
|
||
|
||
- Workspace 是 LangBot 实例内的逻辑租户边界,不是一个 Pod,也不是一个 Kubernetes namespace。
|
||
- OSS 每个 LangBot 实例只能存在一个 Workspace,但该 Workspace 可以有多个用户、邀请和固定角色权限。
|
||
- SaaS 一个账户可以拥有或加入多个 Workspace;全部 Workspace 由同一个逻辑 LangBot 实例承载。
|
||
- 当前实例可以单副本运行;未来 Core、Runtime、Box 或数据库可以在不引入多个产品级 LangBot 实例的前提下横向扩展。
|
||
- SaaS 普通注册会自动创建个人 Workspace。通过邀请注册的新用户也会创建个人 Workspace,并同时加入受邀 Workspace。
|
||
- OSS 首位用户创建实例唯一 Workspace;后续用户通过邀请加入该 Workspace,不再创建第二个 Workspace。
|
||
- 数据隔离、权限校验、运行时隔离必须留在开源 Core 中,不能依赖闭源服务在线代理每次请求。
|
||
- SaaS 全局账户、Workspace 目录、Membership、Invitation、Subscription、Billing 和 Entitlement 由闭源控制面维护;Core 只保存执行所需的 Account、Workspace 和 Membership 版本化投影,不投影 pending Invitation secret。
|
||
- 现有 Space 的 Pod-per-account、namespace-per-account 和 Pod subscription 模型不进入新架构。Cloud 部署按绿地方案重新设计。
|
||
- 现有 Space 只复用账户交互、OAuth、支付适配器、邮件、财务后台和生命周期可靠性经验,不复用旧 Cloud 领域模型。
|
||
|
||
推荐将 Cloud Control Plane 做成独立进程、独立发布、独立数据库和独立信任域的闭源服务。第一阶段可以和 Space 放在同一个私有代码仓库中,但不能作为 LangBot Core 的 Python 闭源补丁,也不应继续把新 Cloud 逻辑堆进现有 Space 单体。
|
||
|
||
## 2. 范围与非目标
|
||
|
||
### 2.1 本方案覆盖
|
||
|
||
- 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 级订阅、权益、用量和计费模型。
|
||
- 单逻辑 LangBot SaaS 实例,以及未来内部 replica、worker 和 database shard 的兼容边界。
|
||
- 分阶段实施和验收策略。
|
||
|
||
### 2.2 本方案不覆盖
|
||
|
||
- 兼容旧 Space 的 Pod、账户 namespace 或旧 CloudPlan 部署方式。
|
||
- 在旧 Pod 模型上原地升级为多租户。
|
||
- 旧 Cloud 客户数据和基础设施的具体迁移执行方案。
|
||
- 第一版自定义角色、SAML、SCIM 或企业离线授权。
|
||
- 第一版跨 Cell active-active。
|
||
- 第一版 database-per-workspace 或 schema-per-workspace。
|
||
|
||
如果未来需要迁移旧 Cloud 的账户、财务记录或客户数据,应单独立项;旧部署模型不作为新架构的设计约束。
|
||
|
||
## 3. 术语与不变量
|
||
|
||
### 3.1 术语
|
||
|
||
| 术语 | 定义 |
|
||
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| 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 本地执行的功能和额度快照 |
|
||
|
||
### 3.2 必须始终成立的不变量
|
||
|
||
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。
|
||
|
||
## 4. 产品行为
|
||
|
||
### 4.1 OSS 与 SaaS 能力矩阵
|
||
|
||
| 能力 | OSS | SaaS |
|
||
| ------------------------------------- | ---------------------------- | --------------------------------------- |
|
||
| 每个 Account 可拥有的 Workspace | 不适用;本地实例只有一个 | 由 ProductPolicy 决定 |
|
||
| 每个 CloudInstance 可承载的 Workspace | 固定 1 | 由 RuntimeClass 和 capacity policy 决定 |
|
||
| Workspace 成员数 | 多用户 | 按 Workspace 权益 |
|
||
| 邀请成员 | 支持 | 支持 |
|
||
| 固定 RBAC | 支持 | 支持 |
|
||
| 自定义角色 | 不支持 | 后续商业能力 |
|
||
| Workspace 创建 | 仅初始化时创建唯一 Workspace | 注册自动创建,也可按权益继续创建 |
|
||
| Workspace 切换 | 无需展示 | 支持 |
|
||
| 全局账户 | 无 | 支持 |
|
||
| 订阅与计费 | 无远端依赖 | 控制面管理 |
|
||
| SSO、SCIM | 不支持 | 后续商业能力 |
|
||
| 租户隔离 | 必须完整实现 | 必须完整实现 |
|
||
|
||
OSS 的 edition policy 应表达为:
|
||
|
||
```text
|
||
workspace_limit = 1
|
||
members_enabled = true
|
||
invitations_enabled = true
|
||
fixed_rbac_enabled = true
|
||
multi_workspace_enabled = false
|
||
```
|
||
|
||
不能再使用 member_limit = 1,也不能通过关闭邀请或关闭 RBAC 来限制 OSS。
|
||
|
||
### 4.2 OSS 注册与邀请
|
||
|
||
首次初始化必须在一个数据库事务内完成:
|
||
|
||
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
|
||
{
|
||
"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
|
||
}
|
||
```
|
||
|
||
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。
|
||
|
||
### 8.5 UsageEvent
|
||
|
||
Usage 采用 append-only、至少一次投递:
|
||
|
||
```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 只用于审计事件发生时执行的权益版本。
|
||
|
||
Core 不在普通请求中同步扣费。未来若需要本地实时额度,可以设计签名 BudgetLease,但不属于 Cloud v2 MVP;模型 token 如果经过中央模型网关,可由网关作为权威计量来源。
|
||
|
||
### 8.6 DesiredState 与 ObservedState
|
||
|
||
Cell Agent 拉取或接收版本化 DesiredState,并幂等 reconcile:
|
||
|
||
- CloudInstance release。
|
||
- RuntimeClass。
|
||
- capacity。
|
||
- route。
|
||
- placement generation。
|
||
|
||
Cell Agent 上报 ObservedState。Space Web API 或 Control Plane Web API 不直接持有并使用集群管理员凭证执行任意 K8s CRUD。
|
||
|
||
Workspace 的安全或管理员状态只由 Directory aggregate 的 revision 决定;订阅状态只由 Entitlement revision 决定;Placement 只由 DesiredState generation 决定。Core 计算三者的最严格有效状态,但任何通道都不能修改另一个通道的字段。
|
||
|
||
## 9. 身份、鉴权与请求上下文
|
||
|
||
### 9.1 上下文分层
|
||
|
||
建议分为四层:
|
||
|
||
```python
|
||
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: 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
|
||
```
|
||
|
||
服务方法不得通过全局变量或最近 Workspace 获取上下文。Gateway 的签名内部 assertion、直接进入 Core 的本地请求、后台任务、Plugin Host 调用和 runtime lease 都必须生成或继承当前 placement_generation。
|
||
|
||
### 9.2 Account token
|
||
|
||
SaaS Account access token 使用非对称签名和短 TTL,至少包含:
|
||
|
||
```json
|
||
{
|
||
"sub": "account_uuid",
|
||
"iss": "cloud_control_plane",
|
||
"aud": "langbot_cloud_edge",
|
||
"iat": 0,
|
||
"exp": 0,
|
||
"jti": "token_uuid",
|
||
"token_version": 3
|
||
}
|
||
```
|
||
|
||
JWT 表示全局账户身份,不直接授予任何 Workspace 权限,也不需要因切换 Workspace 重新签发。公网 Account token 只发给 Edge 或 Cloud BFF,Data Plane 不直接接受该 audience。
|
||
|
||
OSS token 同样应从 email 主体迁移到 account_uuid,并保留 issuer、audience、expiry 和 token version。
|
||
|
||
### 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_bot(self, ctx: RequestContext, bot_uuid: str) -> Bot:
|
||
require(ctx, "bot.view")
|
||
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
|
||
```
|
||
|
||
禁止:
|
||
|
||
- Service 内 select 全表后在 Controller 过滤。
|
||
- 通过可选 workspace_uuid = None 表示系统权限。
|
||
- 使用最近 Workspace 或默认 Workspace 执行后台任务。
|
||
- 仅检查资源 uuid 而不检查 workspace_uuid。
|
||
|
||
需要逐一改造:
|
||
|
||
- Account、Workspace、Membership、Invitation。
|
||
- API Key。
|
||
- Bot 和 Platform。
|
||
- Pipeline 和 RunRecord。
|
||
- ModelProvider 和所有 Model。
|
||
- Plugin installation、configuration 和 page API。
|
||
- MCP。
|
||
- Knowledge 和 RAG。
|
||
- Monitoring。
|
||
- Webhook。
|
||
- Binary Storage。
|
||
|
||
### 15.2 后台任务
|
||
|
||
后台任务 payload 必须包含:
|
||
|
||
- instance_uuid。
|
||
- workspace_uuid。
|
||
- actor 或受约束的 SystemContext。
|
||
- resource_uuid。
|
||
- operation_id。
|
||
- generation。
|
||
|
||
跨所有 Workspace 的平台任务只能使用专门的 SystemContext,并由明确的 allowlist repository 执行,不能复用普通 Service 的 context=None。
|
||
|
||
## 16. 运行时与 SDK 隔离
|
||
|
||
### 16.1 Query、Event 和 Session
|
||
|
||
Query 和 Event 增加 workspace_uuid 用于传递和观测,但它不是授权来源。Host 必须从可信 Bot、Pipeline、Plugin installation 或连接记录推导 Workspace,并校验传入字段一致。
|
||
|
||
Session key 至少为:
|
||
|
||
```text
|
||
workspace_uuid
|
||
bot_uuid
|
||
launcher_type
|
||
launcher_id
|
||
```
|
||
|
||
Query、Session、RuntimeBot、RuntimePipeline、cache key、lock key 和 manager index 都必须包含 Workspace。
|
||
|
||
### 16.2 Plugin
|
||
|
||
SaaS 不允许多个 Workspace 共享同一个第三方插件进程或可信 Host 连接。原因是插件可以保留跨请求内存、启动后台任务并主动调用 Host API。
|
||
|
||
安全不变量:
|
||
|
||
- 一个不可信 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。
|
||
|
||
隔离等级:
|
||
|
||
| 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 启动容器。
|
||
|
||
OSS 因为实例只有一个 Workspace,可以继续使用单一插件 runtime,但协议和存储仍必须 Workspace-aware。
|
||
|
||
### 16.3 Plugin Host API
|
||
|
||
以下 API 必须限定当前 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。
|
||
- workspace storage。
|
||
|
||
跨 Workspace 管理能力不能通过普通 Plugin Host API 暴露。
|
||
|
||
### 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 内校验。
|
||
|
||
### 16.5 Box
|
||
|
||
Box namespace 至少是 instance_uuid 与 workspace_uuid:
|
||
|
||
- Session、Process、目录、端口和 WebSocket attach token 都在该 namespace 内。
|
||
- SaaS 插件不能传任意 host_path。
|
||
- 不允许额外特权挂载。
|
||
- 所谓 global sandbox 只能表示 Workspace 内 global,不能表示整个 CloudInstance global。
|
||
- 配额从 EntitlementSnapshot 获取并在 Core 或 Box gateway 执行。
|
||
|
||
### 16.6 RAG、Vector 和 Object Storage
|
||
|
||
- 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
|
||
|
||
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
|
||
```
|
||
|
||
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。
|
||
|
||
### 17.3 OSS 前端
|
||
|
||
- 不显示 Workspace switcher 和 Create Workspace。
|
||
- 可以显示唯一 Workspace 名称。
|
||
- 提供 Members 和 Invitations 页面。
|
||
- 提供固定角色选择。
|
||
- 初始化页面创建首位 owner 和唯一 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 就认为完成安全隔离。
|
||
|
||
### 17.5 登录后的 SaaS 路径
|
||
|
||
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。
|
||
|
||
## 18. 故障与降级策略
|
||
|
||
### 18.1 不同租约不能混用
|
||
|
||
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 |
|
||
|
||
因此“24 到 72 小时宽限”只适用于 Entitlement 和已有 Bot 等自动化工作负载,不代表用户可以在身份服务或 Directory 断开后继续登录 24 到 72 小时。
|
||
|
||
### 18.2 故障矩阵
|
||
|
||
| 故障 | 交互用户 | 已有 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 只显示故障,不伪造成功 |
|
||
|
||
UsageEvent 在控制面故障时写入 durable outbox。
|
||
|
||
### 18.3 必须 fail closed 的情况
|
||
|
||
- SaaS 首次启动没有有效 InstanceManifest。
|
||
- Manifest 或 Entitlement 签名无效。
|
||
- audience 或 instance_uuid 不匹配。
|
||
- revision 或 placement generation 回滚。
|
||
- 已知 Workspace suspended。
|
||
- 没有本地 Membership 投影。
|
||
- InteractiveDirectoryLease 已过期的交互请求。
|
||
- WorkspaceStatusLease 已过期的 Bot、Webhook、API Key、job 或 runtime 请求。
|
||
- SaaS 请求缺少 Workspace。
|
||
|
||
不得回退到 OSS mode、default Workspace 或全局资源访问。
|
||
|
||
### 18.4 Membership 撤销与 Workspace 暂停
|
||
|
||
- 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 到期
|
||
|
||
宽限期结束后:
|
||
|
||
- 阻止付费功能的新建和扩大资源。
|
||
- 是否停止已有 Bot 应作为独立产品策略,不能由通用 quota 代码隐式决定。
|
||
- 欠费暂停必须可审计并支持恢复。
|
||
|
||
## 19. 安全清单
|
||
|
||
必须防止:
|
||
|
||
- 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。
|
||
|
||
安全原则:
|
||
|
||
- 最小权限。
|
||
- 服务端推导 Workspace。
|
||
- 复合外键。
|
||
- 密钥只保存 hash。
|
||
- 内部通信使用 mTLS 和短期签名 service token。
|
||
- 支付 webhook 先持久化再履约。
|
||
- 所有控制面事件和运行操作幂等。
|
||
- 跨租户访问默认返回 404。
|
||
|
||
## 20. 实施阶段
|
||
|
||
### Phase 0:契约和基线
|
||
|
||
- 冻结术语、产品矩阵和 Source of Truth。
|
||
- 定义 RequestContext、ExecutionContext 和 Provider 接口。
|
||
- 定义 DirectoryEvent、DirectoryLease、InstanceManifest、EntitlementSnapshot、UsageEvent 和 Placement generation schema。
|
||
- 建立跨租户测试辅助工具。
|
||
- 列出所有需要 workspace_uuid 的表、cache、lock、storage 和 runtime key。
|
||
|
||
### Phase 1:OSS tenancy kernel
|
||
|
||
- Account 引入稳定 uuid。
|
||
- 新增 Workspace、Membership、Invitation。
|
||
- 首次初始化创建唯一 Workspace 和 owner。
|
||
- 后续邀请注册加入唯一 Workspace。
|
||
- 固定 RBAC。
|
||
- OSS edition policy 强制 workspace_limit = 1。
|
||
- JWT 从 email 主体迁移到 account_uuid。
|
||
|
||
### Phase 2:资源行级隔离
|
||
|
||
- 按 expand、backfill、validate、contract 迁移所有现有数据到默认 Workspace。
|
||
- 先增加可回填字段,再校验所有行和关联,最后收紧非空与复合外键。
|
||
- Repository 和 Service 强制 scope。
|
||
- API Key、Webhook 和 Monitoring 完成隔离。
|
||
- SQLite 和 PostgreSQL 迁移均可中断重试并支持 forward recovery。
|
||
|
||
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 参数。
|
||
|
||
### 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。
|
||
|
||
### Phase 5:Closed Control Plane 与 Billing
|
||
|
||
- 全局 Account 和 Workspace directory。
|
||
- BillingAccount、Product、Plan、Price 和 Subscription。
|
||
- Payment provider event 和财务模型。
|
||
- Entitlement signer 和 Usage ledger。
|
||
- Cloud Portal 和运营后台。
|
||
|
||
### 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 新用户完整链路。
|
||
|
||
### Phase 7:HA 与规模化
|
||
|
||
- API 与 runtime worker 拆分。
|
||
- Workspace 或 Bot lease。
|
||
- 同 Cell 多 AZ。
|
||
- Workspace migration。
|
||
- 跨 Cell DR。
|
||
|
||
### 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 写入。
|
||
|
||
## 21. 第一批代码改动建议
|
||
|
||
第一批 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 商业能力和全局控制能力留在独立闭源服务中。
|