feat(tenancy): harden shared cloud runtime boundaries

This commit is contained in:
Junyan Qin
2026-07-20 01:47:42 +08:00
parent 41772920ef
commit a47bfe8167
121 changed files with 18152 additions and 5588 deletions
@@ -1,12 +1,13 @@
# Cloud v2 多租户架构决策与待决策项
状态:`PARTIALLY_DECIDED — MVP target confirmed, implementation pending`
状态:`DECIDED — Core isolation kernel implemented; SaaS activation gates remain`
创建日期:2026-07-19
最近更新:2026-07-19
最近更新:2026-07-20
本文记录 Cloud v2 多租户架构中已经确认的首期决策、明确淘汰的方案和仍需在后续阶段决定的扩展项。
“已决定”表示实现目标已经确定,不表示代码已经完成。正式实现时还需要把结论同步到
[implementation-decisions.md](./implementation-decisions.md)、主架构、实施清单、配置模板和协议设计。
本文同时记录实现状态。“实现完成”仅指开源 Core/SDK 的隔离内核和 fail-closed 门禁,
不表示闭源 Control Plane、计费或 Cloud v2 部署已经可上线。最终实现选择同步记录在
[implementation-decisions.md](./implementation-decisions.md),剩余发布门禁记录在实施清单和验证报告。
## 0. 已确认的 SaaS 拓扑前提
@@ -25,12 +26,12 @@
### 0.1 本轮确认的首期决策
| 编号 | 结论 | 首期状态 |
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| D-001 | 一个共享 Plugin Runtime 控制面;每个运行中的 plugin installation 独占一个 nsjail 子进程;只有 digest 相同且已验证的代码/依赖 artifact 可以只读共享 | `DECIDED — implementation pending` |
| D-002 | 一个共享 Box RuntimeCloud 固定使用 nsjail;符合套餐的 Workspace 最多一个持久 `global` 逻辑 sandbox,普通执行按需启动 nsjail 进程 | `DECIDED — implementation pending` |
| D-003 | SaaS 业务数据使用 PostgreSQL shared schema、应用层作用域和 RLS 双重隔离;pgvector 使用同一 PostgreSQL,作为 SaaS 默认向量后端 | `DECIDED — implementation pending` |
| D-004 | stdio MCP 与 Box availability 解耦;Cloud v2 首期强制关闭 stdio MCP,避免为每个 Workspace 创建额外的 `mcp-shared` persistent sandbox | `DECIDED — implementation pending` |
| 编号 | 结论 | 首期状态 |
| ----- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| D-001 | 一个共享 Plugin Runtime 控制面;每个运行中的 plugin installation 独占一个 nsjail 子进程;只有 digest 相同且已验证的代码 artifact 可以只读共享 | `IMPLEMENTED — real Linux/egress deployment gate pending` |
| D-002 | 一个共享 Box RuntimeCloud 固定使用 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` |
Workspace 的具体创建、释放、数据导出和单 Workspace 恢复机制不在本轮决定;本文只保证这些后续能力不会改变稳定的
`workspace_uuid`,也不会要求重建租户专属部署。
@@ -81,21 +82,18 @@ Core 不能继承 Plugin Runtime 所需的 nsjail/cgroup 权限。Box Runtime
## 3. D-001Plugin Runtime 多租户控制面
状态:`DECIDED — MVP target confirmed, implementation pending`
状态:`IMPLEMENTED — Cloud deployment verification pending`
### 3.1 当前实现事实
### 3.1 已实现的基础
- SDK `RuntimeContext` 和 Core `PluginRuntimeConnector` 当前都绑定一个 WorkspaceRuntime 只有一个全局 PluginManager
- 插件已经是直接子进程,但当前由 `asyncio.create_subprocess_exec` 启动,没有 nsjail 或同等级内核隔离
- 插件包目录当前按 author/name 组织,依赖安装会修改 Runtime 的全局 Python 环境,不能直接作为共享多租户写入边界
- 生产 `run-plugin` 路径仍会读取插件目录中的 `.env`;公开 SaaS 不能让 artifact 自行注入 Runtime secret
- 当前控制协议已有 `SET_RUNTIME_CONFIG`,可以由 Core 把验证后的实例级 worker policy 下发给 Runtime
但该控制 handler 当前仍要求单 Workspace context;目标协议必须先解除控制连接的 Workspace 绑定。
- LangBot 的配置加载器原生支持把 `A__B__C` 环境变量映射到 `data/config.yaml` 的嵌套字段;
数值和布尔字段必须先出现在配置模板中,才能稳定保留类型
- 现有镜像和 Box backend 已包含 nsjail、mount namespace、private `/proc`、rlimit 和 cgroup v2 相关实现,可复用隔离参数生成逻辑。
- 当前 installation identity 及持久化模型还没有完整的随机 `installation_uuid``artifact_digest``runtime_revision`
和 desired-state 字段;它们是目标模型,不是现有能力。
- Plugin Runtime 控制连接只绑定稳定实例身份;一个 Supervisor 通过完整 installation binding 管理多个 Workspace
- 每个 enabled installation 使用独立 nsjail worker;代码只读,home/tmp/data 私有,shared profile 不读取 artifact `.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 storageRuntime 本地缓存丢失后可由 Core replay。
- 相同 digest 的代码和 Runtime 准备的只读依赖环境可以共享,但 worker、运行时写入、配置和数据不合并;
dependency preparation 在启动 worker 前完成,失败会进入明确的 installation failed 状态。
- Cloud shared profile 强制 Linux nsjail`plugin.worker.require_hard_limits=true` 时 cgroup v2 delegation 不可用会启动失败
### 3.2 已确认的不变量
@@ -132,7 +130,7 @@ Core 不能继承 Plugin Runtime 所需的 nsjail/cgroup 权限。Box Runtime
```text
data/plugin-runtime/
├── artifacts/sha256/<artifact_digest>/code/ # digest 校验后只读共享
├── envs/<environment_digest>/ # 可选,只读共享依赖环境
├── environments/sha256/<environment_digest>/ # 原子发布、只读共享依赖环境
└── installations/<installation_uuid>/
├── home/ # 私有可写
├── tmp/ # 私有可写、可清理
@@ -140,8 +138,10 @@ data/plugin-runtime/
```
- artifact 只有在内容摘要和完整性校验一致时才允许共享,不能只凭 author/name/version 复用目录;
cache 接受哪些签名/来源以及如何撤销和 GC,仍是实现前需要补充的供应链规则
- artifact 与共享依赖环境以只读 mount 进入 nsjailinstallation 的 home/tmp/data 使用独立可写 mount。
cache 接受签名/来源撤销和 GC 规范属于后续发布规则,不改变本轮基于已验证 digest 的只读共享边界
- artifact 与按环境摘要构建的共享依赖环境以只读 mount 进入 nsjailinstallation 的 home/tmp/data 使用独立可写 mount。
环境摘要包含 artifact、requirements、Python ABI、Runtime 版本和 installer schema。依赖只能从已验证 artifact 的 PEP 508 声明构建,
index/trusted-host 只由实例配置控制;构建在独立 nsjail 的临时路径中完成并在成功后原子发布,失败或并发安装不能留下可见半成品。
- 插件 cwd 可以是其私有 mount namespace 内的只读 `/plugin`,不要求为每个 installation 复制代码;
必须私有的是 home/tmp/data 等所有可写路径。
- nsjail 必须启用 mount、PID、IPC、UTS 和 private `/proc` 等必要 namespace,插件不能枚举或 signal 其他插件及 Runtime 进程,
@@ -164,6 +164,7 @@ plugin:
max_pids: 128
max_open_files: 256
max_file_size_mb: 512
require_hard_limits: true # Cloud; OSS defaults false
```
配置文件路径为 `data/config.yaml`,沿用现有原生环境变量覆写:
@@ -173,6 +174,7 @@ plugin:
- `PLUGIN__WORKER__MAX_PIDS`
- `PLUGIN__WORKER__MAX_OPEN_FILES`
- `PLUGIN__WORKER__MAX_FILE_SIZE_MB`
- `PLUGIN__WORKER__REQUIRE_HARD_LIMITS`
Core 启动时校验配置并通过现有 `SET_RUNTIME_CONFIG` 下发不可变 `PluginWorkerPolicy`
Runtime 不读取另一份环境变量配置,避免两个配置源不一致。CPU、内存和 PID 使用 cgroup 硬限制,
@@ -203,22 +205,23 @@ installation data 的总空间硬配额需要 filesystem project quota 或独立
- 修改 manifest 不能改变任何资源上限。
- 旧 generation/revision 的回调、消息、副作用和存储访问全部失败关闭。
- Runtime 重启能从业务 desired state 恢复,不依赖本地进程表作为权威真相。
- requirements 中存在 Runtime 基础镜像未预装的包时,Supervisor 仍能先完成共享依赖环境准备再启动 worker;
安装失败不会留下持续重启的半启动进程,也不会影响同 digest 已就绪环境的其他 installation。
## 4. D-002Box 多租户控制面和套餐边界
状态:`DECIDED — MVP target confirmed, implementation pending`
状态:`IMPLEMENTED FAIL-CLOSED — production quota provider pending`
### 4.1 当前实现事实
### 4.1 已实现的基础
- Box 已经按 `instance_uuid + workspace_uuid` 派生持久 namespace,并按 generation 派生运行 namespace
单个 server/control connection 已有服务多个 Workspace 的数据结构基础
- 当前 nsjail backend 的 session 是“逻辑 session + 共享 host workspace 目录”。每次普通 `exec` 都启动一个 one-shot nsjail 进程,
命令结束后进程退出;managed process 才会保持对应 nsjail 进程运行
- `persistent=True` 会让 session 跳过 300 秒 TTL reaper 和普通 shutdown 清理,但 Runtime 重启不会恢复内存中的 session 或运行进程
- 现有 `{global}` 强制 session ID 只覆盖部分 Agent 执行入口,不能单独保证所有入口最多一个 session,也不会自动设置 `persistent=True`
- 当前没有 `max_sessions_per_workspace`,调用其他入口仍可能使用不同逻辑 session ID,必须在 Box admission 层补最终门禁
- nsjail 文件机制不是对象存储同步算法,而是把 Box Runtime 容器中的 Workspace host path bind mount 到 sandbox 的 `/workspace`
- 当前 nsjail 在不可写/不可用的 cgroup v2 环境会降级并告警,无法保证共享 SaaS 的硬内存隔离。
- 共享 Box 控制连接可服务多个 Workspace;所有操作绑定 instance、Workspace generationRuntime namespace 由可信 context 派生。
- 短期 `SandboxAdmissionGrant`、revision tombstone 和原子 session admission 强制每个合资格 Workspace 最多一个 `global` persistent sessionmanaged process 固定为零
- Core 与 Runtime 使用认证 host-control challenge 校验同一个 durable volume,而不是比较路径字符串;不一致时启动和重连失败。
- Cloud skill 只传逻辑名称;Runtime 从 Workspace-scoped store 解析只读包路径,Python env/cache 写入租户自己的 `/workspace/.skill-envs`
- ZIP 安装限制压缩输入、条目、单项、总解压量和压缩比,采用流式解压并拒绝 link、非普通文件、重复项和路径逃逸
- 附件 host path 使用 query UUID 和 dirfd/openat/O_NOFOLLOWCloud replica 启动不再全局清理其他请求目录,遍历和删除有 inode 预算
- grant-enforced readiness 强制 cgroup、namespace、mount、共享卷、Workspace hard quota、Skill hard quota、ephemeral storage 和 inode quota 全部被证明
普通 nsjail backend 对尚未实现的硬磁盘能力明确返回 false,因此当前 Cloud Box 会按设计拒绝启动,直到新部署提供真实 quota provider
### 4.2 首期套餐与 entitlement 模型
@@ -307,23 +310,50 @@ installation data 的总空间硬配额需要 filesystem project quota 或独立
- 非 Pro、entitlement 缺失/过期及伪造 plan 的 API 直调全部失败关闭。
- TTL 不回收 persistent sessionRuntime 重启后进程和临时目录失效,但 `/workspace` 保留并能在下一次使用时安全重建。
- 两个 Workspace 的文件、进程、session、attach token 和 generation 完全隔离;network/managed-process 请求在首期失败关闭。
- Core 与 Box Runtime 使用同一路径的共享持久卷;filesystem quota 在 Box Runtime 写入点真实生效,现有目录扫描不被当作硬配额
- cgroup hard limit 不可用时 Cloud Box Runtime readiness 失败
- Core 与 Box Runtime 通过随机 marker challenge 证明同一共享持久卷;只配置相同路径字符串不算通过
- Workspace、Skill store、ephemeral root/tmp/home 的 byte quota 与 inode quota 在写入点真实生效;现有目录扫描不被当作硬配额
- cgroup 或任一硬存储能力不可用时 Cloud Box Runtime readiness 失败;普通 nsjail 因此不会被误当成 production-ready provider。
## 5. D-003SaaS PostgreSQL 与 pgvector
状态:`DECIDED — MVP target confirmed, implementation pending`
状态:`PARTIALLY IMPLEMENTED — shared schema/pgvector complete; SaaS transaction and deployment gates remain`
当前实现仍有明确差距:Cloud 模板默认 Chromapgvector adapter 使用独立 engine、全局 `id` 和固定 `vector(1536)`
没有持久化 `workspace_uuid/knowledge_base_uuid`,并会在应用启动时执行 `CREATE EXTENSION/create_all`
当前 persistence helper 也不能保证 `SET LOCAL` 与业务查询处于同一 transaction。以下均是目标约束,不是现状描述。
当前分支已实现 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。
### 5.1 已确认的数据库边界
- PostgreSQL 是 SaaS 的业务数据库,不把它扩展成通用 Runtime coordinator、Box session directory 或新控制面数据库。
- M0 使用一个 PostgreSQL business database/shared schema 承载全部 Workspace。创建 Workspace 不创建 database、schema、role 或专属连接池。
- 首版 migrator URL 和 runtime URL 必须归一化到同一 host、port 和 database,但必须使用不同 role。
未来如果 migrator 使用 direct endpoint、runtime 使用 pooler endpoint,只能在两个端点都验证同一个数据库内部 cluster identity 后放宽 host/port 相等。
该 identity 由 migrator 所有并固定,runtime role 只能读取,不能创建、修改或伪造。
- 首版唯一业务 schema 固定为 `public`。migrator 和 runtime 连接都必须满足
`current_schema() = 'public'``current_schemas(false) = ARRAY['public']`;禁止 runtime role 级和 business database 级 `search_path` 覆写。
- migrator 和 runtime session 的安全值固定为 `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 是第二道边界。
- 应用角色不得拥有 `BYPASSRLS`,关键租户表使用 `FORCE ROW LEVEL SECURITY`migration/repair/audit 使用独立受控角色。
- runtime role 的直接 ACL 固定为:business database 的 `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`
- runtime role 必须是 `LOGIN`,但不得具有 superuser、`BYPASSRLS``CREATEDB``CREATEROLE` 或 replication 属性;
不得在 role membership 中以 granted role、member 或 grantor 任一方向出现;不得拥有 database、schema、table、view、sequence、routine 或 extension
不得持有 column ACL,也不得使用、创建或拥有其他非系统 schema。
- business database 必须安装 `vector`,且 extension catalog 只允许 `plpgsql``vector`;不得存在 FDW、foreign server 或 user mapping。
runtime role 和 `PUBLIC` 都不得有显式 routine ACL 或 parameter `SET/ALTER SYSTEM` ACLruntime role 不得有效执行任何
`SECURITY DEFINER` routine,包括被 allowlisted extension 收编的 routine。普通非 `SECURITY DEFINER` 内建函数的隐式执行权限不在此禁令内。
- PostgreSQL 新 database 默认向 `PUBLIC` 提供的 `TEMP` 是首版在专用业务 database 上明确接受的兼容性决定,
不是 migrator 对 runtime role 的直接 grant;首版不得据此把业务 database 与不受信任工作负载混用。
migrator 在释放 advisory lock 前建立并校验上述精确 allowlistCloud runtime 每次启动都必须重新完成 schema、身份、有效权限和 catalog 负向校验,发现 drift 立即失败关闭。
- PostgreSQL role 是 cluster-wide identity,当前 database 内的 catalog audit 不能证明同一 credential 无法连接 cluster 中的其他 database。
SaaS 生产环境必须使用仅向该 credential 暴露目标 business database 的专用 PostgreSQL cluster/endpoint
或通过已验证的 HBA/proxy policy 证明该 credential 只能连接目标 business database;这项部署隔离仍是未完成的 activation gate。
- 关键租户表使用 `FORCE ROW LEVEL SECURITY`migration/repair/audit 使用独立受控 migrator role。
- 每个租户事务通过 `SET LOCAL` 设置 tenant context,并由统一 `TenantUnitOfWork` 保证设置 context 和业务查询使用同一事务/连接。
禁止使用连接级 session variable 或 `search_path`,避免连接池、PgBouncer、异常回滚和后台任务串租户。
- 一个 `TenantUnitOfWork` 只能访问一个 Workspace。业务写入与对应 business outbox 在同一事务中提交;
@@ -344,6 +374,9 @@ installation data 的总空间硬配额需要 filesystem project quota 或独立
`CHECK (vector_dims(embedding) = embedding_dimension)` 校验;release migration 为允许的维度创建带 dimension predicate 的 expression/partial ANN index。
知识库/model 元数据必须选择已启用维度,写入和查询 mismatch 或未启用维度时失败关闭,不能截断、补齐、退化为无界扫描或换后端。
- `vector` extension、表、索引和 RLS 由 release migration 创建。应用进程不在启动时执行 DDL。
- 0013 如需搬迁 legacy pgvector 数据,migrator 作为源表 owner 先记录每个受保护源表的 `ENABLE/FORCE RLS` 状态,
仅在同一 migration transaction 内临时暂停 RLS,并在 `finally` 中精确恢复各表原状态。
该流程不依赖 superuser 或 `BYPASSRLS`,也不允许在迁移事务外留下已禁用的 RLS。
### 5.3 候选拓扑与未来演进
@@ -357,6 +390,7 @@ installation data 的总空间硬配额需要 filesystem project quota 或独立
M0 不提前增加始终返回 `primary` 的 resolver、shard router 或 shard binding。
P1 的 resolver、映射、在线迁移、连接池预算、shard-affine replica 和 dedicated shard 细节等到出现容量、地域或合规需求时再设计。
在此之前,direct endpoint 与 pooler endpoint 分离只能通过数据库内部、runtime 不可伪造的 cluster identity 开启,不使用 DNS 名、数据库名或配置声明代替。
### 5.4 备份与生命周期边界
@@ -373,6 +407,15 @@ P1 的 resolver、映射、在线迁移、连接池预算、shard-affine replica
- 故意遗漏应用层 Workspace filter 时,RLS 仍阻止跨租户读写。
- 连接池/事务池复用、异常回滚、并发请求和后台任务不会残留 tenant context;如部署 PgBouncer,也必须覆盖 transaction pooling。
- migration 对 shared schema 只执行一次,不产生 Workspace 级 schema drift;应用启动角色不能执行 DDL。
- 首版拒绝 host、port 或 database 不同的 migrator/runtime URL,并拒绝相同 rolemigrator/runtime 都只解析到 `public`
migrator 在迁移后完成精确 table/sequence/`alembic_version` ACL grant 和正反向 role 校验,runtime 每次启动重新校验。
- runtime role 没有任一方向的 role membership、`WITH GRANT OPTION``search_path` 覆写、对象所有权、其他 schema 访问或非业务对象权限;
专用业务 database 上可继承 PostgreSQL 默认 `PUBLIC TEMP`,但 runtime role 没有直接 `TEMP` ACL。
- migrator/runtime session GUC 保持 `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。
- 生产 deployment 证明 cluster-wide runtime credential 只能连接目标 business database;专用 cluster/endpoint 或 HBA/proxy 隔离未经验证前不得启用 SaaS。
- legacy pgvector 搬迁在非 superuser、非 `BYPASSRLS` 的 table-owner migrator 下可成功,成功、异常和重试路径都精确恢复所有源表的 RLS/FORCE 状态。
- 业务写入和对应 business outbox 在同一事务内具备可证明的提交顺序;外部 generation/fencing token 校验失败时不产生写入。
- pgvector 使用真实 PostgreSQL 集成测试覆盖:两个 Workspace 使用相同 `vector_id`、猜测其他 Workspace ID、
故意遗漏 scope、连接复用、CRUD 和后台任务,全部不能越权。
@@ -381,13 +424,13 @@ P1 的 resolver、映射、在线迁移、连接池预算、shard-affine replica
## 6. D-004stdio MCP 独立开关
状态:`DECIDED — MVP target confirmed, implementation pending`
状态:`IMPLEMENTED`
### 6.1 当前问题
### 6.1 已修复的原问题
- 当前 stdio MCP 的启用条件只检查 transport 为 `stdio` 且 Box available,没有独立 feature gate。
- 所有 stdio MCP 当前使用固定的 `mcp-shared` 逻辑 session,并强制 `persistent=True`
- 如果多租户 Cloud 通过 `box.enabled` 开放能力,每个配置 stdio MCP 的 Workspace 都会额外保留一个 persistent sandbox
- 修复前,stdio MCP 的启用条件只检查 transport 为 `stdio` 且 Box available,没有独立 feature gate。
- 修复前,所有 stdio MCP 使用固定的 `mcp-shared` 逻辑 session,并强制 `persistent=True`
- 在该旧逻辑下,如果多租户 Cloud 通过 `box.enabled` 开放能力,每个配置 stdio MCP 的 Workspace 都会额外保留一个 persistent sandbox
绕过“每 Workspace 最多一个 managed `global` sandbox”的成本和套餐边界。
### 6.2 首期决定