docs/multi-tenant/pending-architecture-decisions.md
状态:DECIDED — Core isolation kernel implemented; SaaS activation gates remain
创建日期:2026-07-19
最近更新:2026-07-24
本文记录 Cloud v2 多租户架构中已经确认的首期决策、明确淘汰的方案和仍需在后续阶段决定的扩展项。 本文同时记录实现状态。“实现完成”仅指开源 Core/SDK 的隔离内核和 fail-closed 门禁, 不表示闭源 Control Plane、计费或 Cloud v2 部署已经可上线。最终实现选择同步记录在 implementation-decisions.md,剩余发布门禁记录在实施清单和验证报告。
instance_uuid;replica_id、worker_id 和进程地址是短期运行身份,不能写进业务资源的永久主键。workspace_uuid 始终是数据、任务和运行时的租户键,也是未来内部路由与分片的候选键。“单个 LangBot 实例”表示单个逻辑服务和安全域,不等于永远只有一个 OS 进程或一个 Kubernetes replica。
当前代码字段 placement_generation 在完成架构迁移前继续兼容,目标语义和候选命名是 execution_generation。
| 编号 | 结论 | 首期状态 |
|---|---|---|
| D-001 | 一个共享 Plugin Runtime 控制面;每个运行中的 plugin installation 独占一个 nsjail 子进程;只有 digest 相同且已验证的代码 artifact 可以只读共享 | IMPLEMENTED — egress/disk-quota pending; restart-storm fault injection pending |
| D-002 | 一个共享 Box Runtime;Cloud 固定使用 nsjail;符合套餐的 Workspace 最多一个持久 global 逻辑 sandbox,普通执行按需启动 nsjail 进程 | IMPLEMENTED FAIL-CLOSED — hard filesystem quota provider pending |
| D-003 | SaaS 业务数据使用 PostgreSQL shared schema、应用层作用域和 RLS 双重隔离;pgvector 使用同一 PostgreSQL,作为 SaaS 默认向量后端 | PARTIALLY IMPLEMENTED — transaction/outbox/deployment gates remain |
| D-004 | stdio MCP 与 Box availability 解耦;Cloud v2 首期强制关闭 stdio MCP,避免为每个 Workspace 创建额外的 mcp-shared persistent sandbox | IMPLEMENTED |
| D-005 | 目录启动使用事务一致的全量快照,运行时按事件涉及的 Workspace 拉取增量;每个 Core replica 独立消费事件,共享 PostgreSQL 投影和 inbox | IMPLEMENTED — production fault injection pending |
Workspace 的具体创建、释放、数据导出和单 Workspace 恢复机制不在本轮决定;本文只保证这些后续能力不会改变稳定的
workspace_uuid,也不会要求重建租户专属部署。
共享可信控制面和基础设施池,隔离不可信执行单元;减少独立部署、扩缩容和运维组件,使新增 Account 或 Workspace 的静态成本接近零。
这里的“减少组件”指减少独立 Deployment、Service、数据库、消息系统和租户专属常驻控制面, 不是通过合并安全边界来减少必要的隔离进程。
统一评估原则:
workspace_uuid。flowchart LR
Traffic["SaaS traffic"] --> Core["One logical LangBot instance
1 Core replica in MVP"]
Core --> PluginRuntime["Shared Plugin Runtime
trusted supervisor"]
Core --> BoxRuntime["Shared Box Runtime
nsjail backend"]
PluginRuntime --> PA["Workspace A / installation 1
isolated nsjail process"]
PluginRuntime --> PB["Workspace B / installation 2
isolated nsjail process"]
BoxRuntime --> BA["Workspace A
one persistent global logical session"]
BoxRuntime --> BB["Workspace B
one persistent global logical session"]
Core --> PG["Shared PostgreSQL business schema
RLS + pgvector"]
| 档位 | 内部部署形态 | 新 Workspace 静态成本 | 启用条件 |
|---|---|---|---|
| M0. 单副本 MVP | 一个 Core、一个共享 Plugin Runtime、一个共享 Box Runtime、一个 PostgreSQL business database;插件按启用状态运行,托管 sandbox 按首次使用与 entitlement 创建 | 只新增 Workspace 业务行 | 当前已确认目标 |
| M1. 同逻辑实例内部横向扩展 | Core、Plugin Runtime、Box Runtime 按容量增加 replica;运行所有权由内部 lease 和 generation fence 决定;PostgreSQL 可增加共享 shard | 不创建 Workspace 专属部署 | 出现容量或可用性证据后 |
| M2. Dedicated 资源档位 | 特定 workload 使用独享 worker pool、sandbox class 或 PostgreSQL shard,但沿用相同身份、协议、schema 和控制面 | 仅由购买 dedicated 的客户承担 | 合规、数据驻留或超大负载 |
M1 是 M0 的透明扩容,M2 是相同架构下的资源等级;两者都不是新的 LangBot 实例、Cell 或 CloudInstance。
外部 API 只认识稳定的 instance_uuid 和 workspace_uuid,不认识 replica、worker、pool 或 shard。
Plugin Runtime 与 Core 在 M0 使用独立容器和 security context,Core 不能继承 Plugin Runtime 所需的
nsjail/cgroup 权限。当前 Runtime 在进程生命周期内绑定首次认证的 runtime_id,因此 M0 必须把 Core 与
Plugin Runtime 放在同一 rollout/restart unit 中协调重启;在实现受认证 takeover 或 owner lease/fencing 前,
不能单独滚动 Core 并让它接管仍存活的 Runtime。Box Runtime 同样使用独立进程身份和安全配置,
不与 Plugin Runtime 合并成一个高权限进程。
状态:IMPLEMENTED — Cloud egress/disk-quota pending; restart-storm fault injection pending
.env。PluginWorkerPolicy 由 Core 的 data/config.yaml 下发,支持原生环境变量覆写;manifest 不能覆盖。installation_uuid、artifact_digest 和 runtime_revision 已持久化并进入 desired-state、注册、Host API 和 generation/revision fence。.lbpkg 先进入 Workspace-scoped durable binary storage;Runtime 本地缓存丢失后可由 Core replay。plugin.worker.require_hard_limits=true 时 cgroup v2 delegation 不可用会启动失败。(instance_uuid, workspace_uuid, execution_generation, installation_uuid, runtime_revision, artifact_digest),且运行期间不可重绑。artifact_digest 相同且完整性已验证的代码文件和依赖环境可以只读共享;
同名同版本但 digest 不同的 artifact 不能共享。配置、持久数据和运行进程不能共享。instance_uuid 和短期 Runtime identity,不绑定某个 Workspace。
每条 installation desired-state 命令都携带并验证完整的 installation binding;每个 worker action context 在注册后永久绑定该 tuple。首期目标目录模型:
data/plugin-runtime/
├── artifacts/sha256/<artifact_digest>/code/ # digest 校验后只读共享
├── environments/sha256/<environment_digest>/ # 原子发布、只读共享依赖环境
└── installations/<installation_uuid>/
├── home/ # 私有可写
├── tmp/ # 私有可写、可清理
└── data/ # 私有持久数据
/plugin,不要求为每个 installation 复制代码;
必须私有的是 home/tmp/data 等所有可写路径。/proc 等必要 namespace,插件不能枚举或 signal 其他插件及 Runtime 进程,
不能读取 Runtime 文件系统、宿主机路径、其他 installation 目录或平台 metadata endpoint。.env。secret 只能由可信控制面按 installation 注入,且不能进入共享 artifact/cache。首期资源规格完全由 LangBot 实例配置决定,manifest 不能声明、放宽或覆盖资源。以下数值是建议默认值,
最终仍由同一实例的 data/config.yaml 统一配置:
plugin:
worker:
max_cpus: 1.0
max_memory_mb: 512
max_pids: 128
max_open_files: 256
max_file_size_mb: 512
require_hard_limits: true # Cloud; OSS defaults false
配置文件路径为 data/config.yaml,沿用现有原生环境变量覆写:
PLUGIN__WORKER__MAX_CPUSPLUGIN__WORKER__MAX_MEMORY_MBPLUGIN__WORKER__MAX_PIDSPLUGIN__WORKER__MAX_OPEN_FILESPLUGIN__WORKER__MAX_FILE_SIZE_MBPLUGIN__WORKER__MAX_CONCURRENT_RESTARTSPLUGIN__WORKER__RESTART_FAILURE_THRESHOLDPLUGIN__WORKER__RESTART_FAILURE_WINDOW_SECONDSPLUGIN__WORKER__RESTART_CIRCUIT_OPEN_SECONDSPLUGIN__WORKER__REQUIRE_HARD_LIMITSCore 启动时校验配置并通过现有 SET_RUNTIME_CONFIG 下发不可变 PluginWorkerPolicy。
Runtime 不读取另一份环境变量配置,避免两个配置源不一致。CPU、内存和 PID 使用 cgroup 硬限制,
open files/file size 使用 rlimit。Cloud deployment profile 固定使用 nsjail,不能通过插件 manifest 或 SaaS 环境变量降级为普通进程。
installation data 的总空间硬配额需要 filesystem project quota 或独立 quota volume,不能用目录扫描伪装成硬限制;
该字段在选定可原子拒绝写入的存储机制前不进入首期配置。
| 状态 | 方案 | 结论 |
|---|---|---|
| 淘汰 | 每 Workspace 一个 Plugin Runtime | 部署、连接和固定内存随 Workspace 线性增长 |
| 淘汰 | 一个插件进程服务多个 Workspace/installation | 全局状态、本地文件和依赖无法形成可信租户边界 |
| 淘汰 | 同 Workspace 多插件合并到一个 worker | 与“每 installation 独立进程”冲突,扩大故障和权限边界 |
| 淘汰 | manifest 自行声明 CPU、内存或更高限额 | 首期统一执行实例级最大值 |
| MVP 不引入 | Runtime 专用数据库、Redis、Kafka 或独立 scheduler | 当前无容量证据,会增加组件和运维面 |
| 后续演进 | 多 Supervisor replica、owner lease、dedicated pool | 保留接口,达到容量或可用性阈值后再决定具体存储与调度方式 |
架构扩展项包括:Core/Supervisor 是否共置、artifact/venv cache 的签名/来源/撤销/GC 规范、installation data hard-quota provider、 v1 connection 的兼容期限,以及进入多 replica 后的 lease TTL、fencing token 和 owner 转移顺序。 这些不改变“每个运行中的 installation 一个隔离进程”的首期边界。
状态:IMPLEMENTED FAIL-CLOSED — production quota provider pending
SandboxAdmissionGrant、revision tombstone 和原子 session admission 强制每个合资格 Workspace 最多一个 global persistent session,managed process 固定为零。/workspace/.skill-envs。plan == pro。managed_sandbox_sessions = 1,其他套餐为 0。建议 capability 形态:{
"features": {
"managed_sandbox": true,
"external_sandbox": false,
"mcp_stdio": false
},
"limits": {
"managed_sandbox_sessions": 1
}
}
box.enabled 只表示当前 LangBot 实例是否部署了 Box Runtime,不能替代 Workspace entitlement。SandboxAdmissionGrant,绑定
instance_uuid + workspace_uuid + execution_generation + entitlement_revision + expires_at + max_sessions + max_managed_processes。
Runtime 只验证和执行该内部 grant,不理解 Pro 等套餐名称,也不相信业务调用方提交的 plan、session ID 或 host path。box.backend: nsjail。sandbox 直接作为 Box Runtime 容器内的 nsjail 子进程运行,
不创建 nested Docker container、独立 Pod、microVM 或 warm pool,也不挂宿主机 docker.sock。global,并强制 persistent=True;
外部调用方不能选择或覆盖 session ID、persistence、host path 或 backend。global 逻辑 session,
它不被 TTL reaper 回收,其 /workspace 持久保存;普通命令仍按需启动并退出 nsjail 进程,不能承诺一个空闲 OS 进程永久驻留。global session。持久 /workspace 必须继续存在,旧 generation 权限必须失败关闭。network=off,调用方和 WebUI 不能覆盖。当前 network=on 会关闭 nsjail 的独立 network namespace,
不能用于共享 SaaS。未来如需联网,必须先实现每 session 独立 netns 和受控 egress,再单独开放。START_MANAGED_PROCESS,SandboxAdmissionGrant.max_managed_processes 固定为 0;
普通 exec 在同一 Workspace 的 global session 内串行执行。未来开放 resident process 前必须增加数量和聚合 CPU/内存上限。/workspace;
不在首期新增对象存储双向同步服务或文件服务。/workspace 的持久性来自独立 durable host path,而不是 persistent=True;后者只禁止 TTL/普通 shutdown 回收逻辑 session。
Box Runtime 容器必须挂载持久卷,Workspace 数据不能只放在容器可写层。root/tmp/home 可以在 Runtime 重启时丢失。global session 可以复用持久 /workspace,但不同 Workspace 即使使用相同文件名、进程名或逻辑 session ID,
物理 namespace、路径、进程和 capability 也必须完全隔离;Cloud 首期没有可暴露端口或共享网络 namespace。managed_sandbox_sessions 配额,也不能读取其他 Workspace 的凭证。| 状态 | 方案 | 结论 |
|---|---|---|
| 淘汰 | 每 Workspace 一个 Box service | 组件和空闲成本随 Workspace 线性增长 |
| 淘汰 | 多 Workspace 共享一个活 sandbox/session | 不能承载不可信代码 |
| 淘汰为 MVP | Docker、独立 Pod、microVM 或 warm pool | Cloud v2 首期固定使用 Runtime 容器内 nsjail |
| 淘汰为 MVP | 非 Pro 使用 Cloud managed sandbox | 首期数值 entitlement 为 0 |
| 后续演进 | 多 Box Runtime replica、dedicated pool、BYOK E2B | 保留 provider/ownership 接口,有真实容量或产品需求后实现 |
global session;重复和并发请求都不能产生第二个 session。/workspace 保留并能在下一次使用时安全重建。状态:PARTIALLY IMPLEMENTED — shared schema/pgvector complete; SaaS transaction and deployment gates remain
当前分支已实现 PostgreSQL shared schema、transaction-local scope、FORCE RLS、Cloud runtime 非 DDL 模式、 同业务数据库 pgvector、显式向量维度和 tenant-scoped vector 主键。一次性 migrator 使用独立凭据、advisory lock, 负责建立并校验 runtime role 的最小权限,并完成全量 schema 验证; 普通业务写入贯穿 commit 的 generation-aware fence、与外部副作用同事务的 outbox,以及 generation cutover 后稳定的 durable object 引用尚未实现。 这些 Core 事务原语与生产 Job、凭据发放、备份和回滚流程,以及 runtime credential 的跨 database 连接隔离证明一起, 都是 Cloud v2 的 SaaS activation gate。
public。migrator 和 runtime 连接都必须满足
current_schema() = 'public' 且 current_schemas(false) = ARRAY['public'];禁止 runtime role 级和 business database 级 search_path 覆写。session_replication_role=origin、row_security=on、
lo_compat_privileges=off。runtime role 或当前 business database 作用域内只要存在任意 pg_db_role_setting 持久化设置就失败关闭,
即使该设置当前看似等于安全值也不接受;tenant context 只能通过事务内 SET LOCAL 建立。workspace_uuid;Repository/Service 的应用层 scope 是第一道边界,PostgreSQL RLS 是第二道边界。CONNECT、public 的 USAGE、全部 allowlisted business table 的
SELECT/INSERT/UPDATE/DELETE、alembic_version 的只读 SELECT,以及业务表自有 sequence 的 USAGE/SELECT。
不授予 database/schema CREATE、table TRUNCATE/REFERENCES/TRIGGER、sequence UPDATE、其他对象权限或任何 WITH GRANT OPTION。LOGIN,但不得具有 superuser、BYPASSRLS、CREATEDB、CREATEROLE 或 replication 属性;
不得在 role membership 中以 granted role、member 或 grantor 任一方向出现;不得拥有 database、schema、table、view、sequence、routine 或 extension,
不得持有 column ACL,也不得使用、创建或拥有其他非系统 schema。vector,且 extension catalog 只允许 plpgsql 和 vector;不得存在 FDW、foreign server 或 user mapping。
runtime role 和 PUBLIC 都不得有显式 routine ACL 或 parameter SET/ALTER SYSTEM ACL;runtime role 不得有效执行任何
SECURITY DEFINER routine,包括被 allowlisted extension 收编的 routine。普通非 SECURITY DEFINER 内建函数的隐式执行权限不在此禁令内。PUBLIC 提供的 TEMP 是首版在专用业务 database 上明确接受的兼容性决定,
不是 migrator 对 runtime role 的直接 grant;首版不得据此把业务 database 与不受信任工作负载混用。
migrator 在释放 advisory lock 前建立并校验上述精确 allowlist;Cloud runtime 每次启动都必须重新完成 schema、身份、有效权限和 catalog 负向校验,发现 drift 立即失败关闭。FORCE ROW LEVEL SECURITY;migration/repair/audit 使用独立受控 migrator role。SET LOCAL 设置 tenant context,并由统一 TenantUnitOfWork 保证设置 context 和业务查询使用同一事务/连接。
禁止使用连接级 session variable 或 search_path,避免连接池、PgBouncer、异常回滚和后台任务串租户。TenantUnitOfWork 只能访问一个 Workspace。业务写入与对应 business outbox 在同一事务中提交;
写入可以校验由执行层传入的 generation/fencing token,但 Runtime owner、lease 和 Box session directory 不由业务 PostgreSQL 承担。CREATE EXTENSION、create_all 或自动 migration。workspace_uuid 和 knowledge_base_uuid,并至少以
(workspace_uuid, knowledge_base_uuid, vector_id) 建立唯一键/主键和查询条件;服务端生成的 collection name/hash 不是安全边界。SET LOCAL;
是否复用普通业务 UoW、role 或 connection pool 由实现决定,但 adapter 不能丢弃 tenant metadata。vector(1536) 不能继续作为无条件硬编码。首期使用无 typmod 的 vector 列和显式 embedding_dimension,
以 CHECK (vector_dims(embedding) = embedding_dimension) 校验;release migration 为允许的维度创建带 dimension predicate 的 expression/partial ANN index。
知识库/model 元数据必须选择已启用维度,写入和查询 mismatch 或未启用维度时失败关闭,不能截断、补齐、退化为无界扫描或换后端。vector extension、表、索引和 RLS 由 release migration 创建。应用进程不在启动时执行 DDL。ENABLE/FORCE RLS 状态,
仅在同一 migration transaction 内临时暂停 RLS,并在 finally 中精确恢复各表原状态。
该流程不依赖 superuser 或 BYPASSRLS,也不允许在迁移事务外留下已禁用的 RLS。| 状态 | 方案 | 结论 |
|---|---|---|
| 首期决定 | P0. shared database/shared schema | 一个 pool、一套 migration;应用 scope + RLS |
| 后续演进 | P1. 多 shared database shard | 每个 shard 仍承载多个 Workspace,并使用相同 schema;有容量/地域证据后再设计 |
| 后续例外 | P2. dedicated shard | 只作为合规、驻留或超大 workload 的资源等级,不建立第二套代码路径 |
| 淘汰 | schema/database per Workspace | catalog、pool、migration、备份成本随 Workspace 线性增长 |
| 淘汰 | database/schema per replica/Cell/Instance | 把业务数据拓扑错误绑定到计算副本或已删除的产品实体 |
M0 不提前增加始终返回 primary 的 resolver、shard router 或 shard binding。
P1 的 resolver、映射、在线迁移、连接池预算、shard-affine replica 和 dedicated shard 细节等到出现容量、地域或合规需求时再设计。
在此之前,direct endpoint 与 pooler endpoint 分离只能通过数据库内部、runtime 不可伪造的 cluster identity 开启,不使用 DNS 名、数据库名或配置声明代替。
public,
migrator 在迁移后完成精确 table/sequence/alembic_version ACL grant 和正反向 role 校验,runtime 每次启动重新校验。WITH GRANT OPTION、search_path 覆写、对象所有权、其他 schema 访问或非业务对象权限;
专用业务 database 上可继承 PostgreSQL 默认 PUBLIC TEMP,但 runtime role 没有直接 TEMP ACL。session_replication_role=origin、row_security=on、lo_compat_privileges=off,
runtime role/当前 database 没有任何 pg_db_role_setting;extension 仅为 plpgsql/vector 且 runtime 不拥有 extension,
database 中没有 FDW/server/user mapping、runtime 或 PUBLIC 显式 routine/parameter ACL、runtime-owned routine 或 runtime 可执行的 SECURITY DEFINER routine。BYPASSRLS 的 table-owner migrator 下可成功,成功、异常和重试路径都精确恢复所有源表的 RLS/FORCE 状态。vector_id、猜测其他 Workspace ID、
故意遗漏 scope、连接复用、CRUD 和后台任务,全部不能越权。状态:IMPLEMENTED
stdio 且 Box available,没有独立 feature gate。mcp-shared 逻辑 session,并强制 persistent=True。box.enabled 开放能力,每个配置 stdio MCP 的 Workspace 都会额外保留一个 persistent sandbox,
绕过“每 Workspace 最多一个 managed global sandbox”的成本和套餐边界。新增独立实例配置:
mcp:
stdio:
enabled: true
true,保持当前本地部署兼容;Cloud v2 通过 MCP__STDIO__ENABLED=false 强制关闭。box.enabled、managed_sandbox entitlement 和 sandbox session 数量相互独立,不能从任一条件推导。false 时任何 entitlement 都不能绕过。开关必须同时覆盖:
不能只在 WebUI 隐藏选项。Cloud 配置关闭时,已有 stdio 记录保留但不自动启动,并返回明确的 feature-disabled 错误;
最终 gate 必须位于 Box 分支和 legacy host-stdio 分支之前,不能误报为 box_unavailable,
也不得创建 mcp-shared session 或 stdio 子进程。
box.enabled=true 且 Workspace 拥有一个 managed sandbox,也无法 create/update/test/start 任何 stdio MCP。mcp-shared session、nsjail 进程或额外配额占用。五项决策共同遵循:
多租户共享可信控制面、连接池、只读 artifact 和基础容量;租户独占不可信执行进程、sandbox、secret、可写文件和数据作用域。
instance_uuid、workspace_uuid 和 execution generation;不能依赖进程地址表达身份。workspace_uuid 从第一天就是内部路由与分片候选键。