Files
LangBot/docs/multi-tenant/workspace-multi-user-architecture.md
2026-07-19 16:19:18 +08:00

1757 lines
80 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 placementprovisioning、dormant 或 archived Workspace 可以暂时没有 Placement。OSS Workspace 直接属于本地 LangBot Instance,不使用 Cloud Placement。
2. 一个 CloudInstance 可以承载多个 Workspace。
3. OSS 一个实例最多只有一个 Workspace。
4. Workspace 可以有多个 AccountAccount 在 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 不能授予 ownerowner 转移使用独立流程。
- 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 使用 LocalProviderSaaS 使用本地 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 DesiredStateCore 执行 |
| 运行时授权 | 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 ACLevent batch 使用由 InstanceManifest 授权的 JWS key 签名并绑定 audience。
- 以 workspace_uuid 为主要 partition keyAccount 事件按 account_uuid 分区。
- Control Plane 为每个目标 instance_uuid 维护 gap-free 的逻辑 directory log offset,作为 instance_sequence;不能直接假设 PostgreSQL sequence 或预分配自增 ID 无空洞。
- 顺序只保证到 aggregateCore 以 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 和 typpayload 至少包含:
```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 recordsnapshot 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 和 typkid 不放在业务 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 和 typpayload 至少包含标准 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 BFFData 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 验证 MembershipMembership 的最终判定只由 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 lockSQLite 使用单写事务和显式 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 PlaneData 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_metadatawizard、页面设置和 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_tiershared 或 dedicated。
同一 Product 可以随容量策略变化而放在不同 RuntimeClass;独享实例只是 placement policy,不是新的租户模型。
## 13. 现有 Space 的复用边界
对最新 Space 架构的结论是:外围能力可复用,旧 Cloud 核心模型不可复用。
本次审查基于 langbot-space origin/main 4c1de05778f3,关键事实锚点:
- internal/entities/persistence/account.goAccountResources 仍以 MaxPods 等账户级资源建模。
- internal/entities/persistence/payment.goPaymentOrder 直接包含 PlanType、BillingCycle、PodUUID 和 PodSubdomain。
- internal/entities/persistence/cloud_plan.goCloudPlan 同时保存价格、产品限制、镜像、CPU、内存和 PVC。
- internal/entities/persistence/pod.goPod 同时保存账户、部署和订阅状态。
- internal/service/cloud.gonamespace 仍由 Account UUID 派生,Pod 删除路径会操作账户 namespace。
- internal/controller/tasks/lifecycle_consumer.goretry、heartbeat、generation、fencing 和 reconcile 模式值得复用。
### 13.1 复用与重构矩阵
| 模块 | 结论 | 可复用内容 | 新架构要求 |
| ---------------- | --------------- | ------------------------------------------------ | ------------------------------------------------------------------------ |
| Account 与 OAuth | 部分复用 | GitHub、Google、邮箱验证、Device Flow 和交互 | 采用 Account、AuthIdentity、Sessionprovider 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 使用独立数据 namespaceCore 普通 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 返回实例唯一 WorkspacePOST /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 assertionData 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_sequenceAccount 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 1OSS 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 3Runtime 和 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 4Cloud 协议
- InstanceManifest 和 JWKS。
- SaaS Account、Workspace、Membership projection。
- Directory inbox/outbox。
- instance_sequence、snapshot reconciliation、InteractiveDirectoryLease 和 WorkspaceStatusLease。
- Entitlement cache 和 quota enforcement。
- Usage outbox。
### Phase 5Closed Control Plane 与 Billing
- 全局 Account 和 Workspace directory。
- BillingAccount、Product、Plan、Price 和 Subscription。
- Payment provider event 和财务模型。
- Entitlement signer 和 Usage ledger。
- Cloud Portal 和运营后台。
### Phase 6Cloud 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 7HA 与规模化
- 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_versionCore 只接受自身声明支持且通过 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 只允许一个 WorkspaceCI 也必须使用测试 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 PlaneCore 使用本地版本化投影。
- Subscription 以 Workspace 为 subjectBillingAccount 作为付款主体。
- 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 承载多个 Workspaceshared 或 dedicated 只是 placement 策略。
这样既能让 OSS 成为完整、安全的多人协作产品,也能把真正的 SaaS 商业能力和全局控制能力留在独立闭源服务中。