chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)

- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
This commit is contained in:
admin committed 2026-10-10 23:13:22 +08:00
1 parent 30b46dbd0c
commit c1b5e4d966
735 files changed
+153192 -2415

No files matched your search

@@ -0,0 +1,922 @@
# 全功能承载判定 —— 底层插件能否承整个项目 · 2026-09-26
> **判定对象**:七项功能特性 —— 多租户(含实例恢复)· 共享模型 · 私有模型(含 agent cli 通过客户端接入)· 三方技能 · 插件使用管理(重点:更新 → 告知用户手动 / 设置自动处理)· 覆盖网络 · IM 对话。
> **判定依据**:本仓代码实测(模块 / 端点级锚点)+ 覆盖网络定稿 `dsh-server-docs/02-架构设计/覆盖网络-顶层架构全貌.md` + 本线已出四份成品件(总盘点 / 能做不能做清单 / 节点与端关系图 / 五场景明确件)。
> ⛔ 本件不动代码、不动服务器;只做判定与缺口登记。
> 📌 口径提醒:本件里的「插件」= **跑在用户实例内的 DSH 插件**(profile 层,R2 允许的唯一扩展点),**不是**平台进程里的模块。
---
## §0 一句话判定
**能承「使用面」,不能承「承载面」。**
| | 内容 | 覆盖率 |
|---|---|---|
| ✅ **能承** | 入口 · 界面 · 配置 · 账本 · 告知 | 七项特性的**使用面近似 100%** |
| ❌ **不能承** | 起实例 / 起窗口首屏 / 取身份 / 把包装进去 / 原生网络守护 / 起承载服务(Manager·Worker 本体) | **引导层 6 件,0%** |
⇒ **「整个项目」不能被一个底层插件单独承起来。** 成立的形态是三层:**薄壳(官方 DSH)+ 引导层(6 件)+ 一个能力包(插件)**。缺任一层都不成立。
⇒ 换个说法:插件承的是**项目形态**,不是**项目本身** —— 每个用户仍旧需要一个实例、每个区仍旧需要一个 Manager,插件让这两样"看得见、点得到",但**不会让它们变少**。
### 一条判据(本件全部判定都用它,别另立)
> **「这件事发生在『插件存在之前』,还是『之后』?」**
> **之前** ⇒ 引导层(=插件还没被装进去,它当然做不了);**之后** ⇒ 可插件化。
---
## §1 七项功能逐项判定(主表)
| 功能 | 插件能承什么(✅) | 插件承不了什么(❌ / ⚠️) | 依据锚点 |
|---|---|---|---|
| **① 多租户(含实例恢复)** | 登录与身份(`/api/auth/*`);实例面板:启停重启+状态(`POST /api/dsh/launch` 130 / `restart` 152 / `stop` 177、`GET /api/dsh/status` 205);文件面(`GET /api/desktop/tree`、`POST /api/fs/mkdir\|upload\|create`、`GET /api/fs/download`);会话与设备台账(`GET /api/auth/sessions` 59、`POST …/revoke` 85、`logout-all` 113、`GET /api/overlay/my-devices` 136) | **起实例本体**(systemd scope `dsh-<uid>-<hash>.scope` + 确定性 uid + cgroup `MemoryHigh/MemoryMax`);**崩溃自动重拉 + 熔断冷却**(指数退避、熔断后冷却窗)。两者都发生在"实例还不存在 / 已经死了"的时刻 ⇒ 那时插件也不存在 | `src/supervisor/orchestrator.ts`|`src/supervisor/crash-policy.ts`(`decideCrashAction` / `breakerActive`)|`src/web/routes/dsh.ts`|`src/web/routes/sessions.ts` |
| **② 共享模型** | 用户侧开关(`POST /api/me/models/shared` 617);可选供应方列表(`GET /api/me/model-providers` 505)、`GET /api/me/keys` 484 | ⚠️ **门禁属管理员**:`shared_model_granted` 默认 0、写入口 `/api/admin/users/:id/shared-model`(admin.ts 107)—— **插件不得代替 admin 授权**(两列两个主体,⛔ 不共用写入口);把「已启用」列表注进实例的那一步在平台 spawn 侧 | `src/web/routes/auth.ts`|`admin.ts:99-109`|`src/db/schema.ts:450-466`|`src/web/server.ts:143` |
| **③ 私有模型** | 模型 / 密钥的增删改查与切换界面:`GET/POST /api/me/keys`(484 / 526)、`:id/toggle` 598、`:id/select` 657、`DELETE :id` 664 | 密钥**落库**(`credential_vault`)与**写进实例 home**(spawn 时铺 `settings.yaml` / `.credentials.yaml`)。🔴 关键推论:**用户自配凭据只有实例内可用** ⇒ **凡是要用模型的能力,插件必须在实例内跑** —— 这恰是插件跑在实例内的根本理由 | `src/db/repo.ts:376/422/429/461`|`src/db/pg.ts:603-701`|`src/fs/user-fs.ts`(`HOME_FILE_NAMES`) |
| **④ agent cli(通过客户端接入)** | — | 🔴 **平台侧零实现,属本件新具名缺口**:`grep -rn "agentCli\|agent-cli\|agent_cli" src/` = **0 命中**;且 `user-fs.ts` 明确本版**没有"实例 → 平台"的身份通道**(唯一那份 `.overlay-device.json` 由控制面代签)。⇒ **不能靠"开发一个插件"补上**,须先设计:客户端侧起/守护 agent CLI 进程 + 一条实例到平台的身份通路 | `src/web/routes/skills.ts`(全仓对照)|`src/fs/user-fs.ts` 模块注释|`src/db/schema.ts`(v13 `overlay_devices.kind`) |
| **⑤ 三方技能** | 安装 / 卸载界面与两阶段冲突流:`/api/skills/shared`(admin-only,rank 600 只读)、`/api/skills/mine`(requireAuth,rank 400);ZIP-only 契约、`conflict:true`+`stagedId` → apply 原子替换;watch 式发现(免重启) | ⚠️ **共享技能面 admin-only** ⇒ 插件只能**请求**、不能代替;**把技能落到目标 home** 必须走 `UserFs` seam(在 worker 上直写 `fs` = **静默空操作**) | `src/web/routes/skills.ts`(430/438/474/490/539/559/598/619/669)|`src/fs/user-fs.ts` |
| **⑥ 插件使用管理(更新)** | 见 **§2.1 展开**(四条分开判,两条 ✅ 两条 ❌) | 同 §2.1 | `src/supervisor/plugin-assembly.ts`|`src/worker/agent.ts`|`src/web/routes/business-plugins.ts` |
| **⑦ 覆盖网络** | 设备侧入网引导 + 台账界面(`POST /api/overlay/device-grant` 59);管理面(`GET /api/admin/overlay-nodes` 73、`/direct` 100/126、`GET /api/admin/overlay-devices` 155、`POST …/revoke` 189,全 `requireAdmin`) | **入网三级链与签名校验**(`directory.ts`:签名不对 ⇒ **失败关闭**);**relay 准入白名单**(`registry.ts` `deriveDialers()` **默认拒绝** ⇒ 结构性隔离);**中继 / 骨干本体**;**TUN 与原生网络守护**。四件都在"还没入网 / 还没有身份"的时刻 ⇒ 引导层 | `src/net/relay/{directory,registry,network,device-grant}.ts`|定稿 §二 · §5.1 |
| **⑧ IM 对话** | **四面齐全,全项目插件纵深最深的一条** —— ① 装配(profile `link:`)② 运行(实例内)③ 取数(`/api/im/data/*` + 三层授权)④ **回调(09-26 反向通道:`register` → `bridge/pull` 长轮询 → `bridge/result` 回投)**;房间 `/api/im/rooms*`(753/805/852/877/926/947)、消息 `/rooms/:id/messages`(970/999)、板面 `/rooms/:id/panels`(1075/1099) | **房间 / 成员 / 消息的权威落库**(`im/store.ts`);**agent 代答的判据**在平台(`agent-bridge.ts` 四条硬约束:禁用自激 / 每轮预算 / 静默期 / 房间级速率+排队)—— ⚠️ 但**拨出与重连在插件侧**(模块头原文)。⚠️ 已拍板代价:E2EE 强度档 ⇒ **agent 代答须重做**、群聊须 Megolm 化 | `src/im/agent-bridge.ts`|`src/im/store.ts`|`src/web/routes/im.ts:333/475`|`src/im/sdk/` |
> ✅ **总览**:七项里 **使用面全部可承**;**承载面(起/装/守/判/搬)全部不可承**。分水岭就是 §0 那条判据。
---
## §2 重点项展开
### §2.1 插件使用管理 —— 「更新 + 告知用户手动 / 设置自动处理」
用户点名的重点。**必须拆成四条分开判,不能合成一条答"能/不能"。**
| # | 能力 | 判定 | 为什么 |
|---|---|---|---|
| 1 | **看见「有新版本」** | ✅ **插件能做** | 判版本只看 `treeSha256`(⛔ 从不看版本号);`GET /api/plugins/shared/status` 矩阵已落地,通道 / 钉版 / 现役三者关系可读。插件拿这份数据渲染成"可更新"提示即可 |
| 2 | **点「更新」按钮** | ✅ **插件能做,但只到「请求」为止** | 插件发出请求、显示进度、显示结果、失败时给原因 |
| 3 | **把新包搬进来**(真正拉取) | ❌ **引导层** | 拉取器 = 引导层第 4 件「把包装进去」的**运行时对照面**:要在**实例没起**时也能跑、要写到**本机**共享只读层、要能重试与留痕(`worker/agent.ts` 的超时解耦 / 动态预算 / 只重试瞬态 / 失败留痕)。插件做不到其中任何一条的前提 |
| 4 | **「设置自动处理」** | ❌ **引导层(插件只能存偏好 + 触发)** | 🔴 **本件最锋利的一条**:自动化的执行者必须**活在被自动化的对象之外**。插件跑在实例内 ⇒ **实例没起 = 插件不存在 = 自动化不成立** ⇒ "关着实例就不更新"、"更新完需要重启却没人重启"。⇒ 自动更新的**执行者只能是引导层**,插件能做的只是写一条偏好、显示一条状态 |
⇒ **一句话给用户**:**「告知」和「按钮」可以全给插件;「拉包」和「自动」必须在插件外面。** 这四条里前两条是产品体验,后两条是物理约束。
### §2.2 多租户与实例恢复 —— 分清两种"恢复"
| 恢复的两种含义 | 判定 | 依据 |
|---|---|---|
| **崩溃后自动重拉**(进程死了再来) | ❌ 引导层 / 服务器侧 | `crash-policy.ts`:指数退避 + 熔断 + **跨轮存活冷却窗**(防"崩溃循环可无限重来")—— 全在"插件不存在"的时刻 |
| **手动复位 / 迁到别处**(跨节点迁移) | ⚠️ **机制未设计**,插件只做入口 | 承载归属(实例的家在哪台机)**尚无迁移设计** ⇒ 本线**只登记、⛔ 不立项** |
⇒ 插件能给的是**「看得见 + 点得动」**(状态、重启、停止、日志入口);**"它自己会好"这件事不在插件里**。
### §2.3 私有模型 / 共享模型 —— 两者不是一回事,别一起答
- **私有模型**:平台库存凭据(`credential_vault`),**spawn 时铺进实例 home**。插件在实例内 ⇒ **插件能直接用**(这正是"插件必须在实例内"的最大理由)。插件界面走 `/api/me/keys*`。
- **共享模型**:**两列两个主体** —— `shared_model_granted` = **管理员授权**(默认 0,用户改不了);`shared_model_enabled` = **用户偏好**。⚠️ 代码里明确写了「⛔ 不能共用一个写入口」。⇒ 插件的界面**只能碰用户偏好那一列**,授权那一列归 admin。
### §2.4 agent cli(通过客户端接入)—— 新具名缺口 🔴
- **实测**:全仓 `agentCli|agent-cli|agent_cli` = **0 命中**。平台侧**没有这条接入面**。
- **连带**:`user-fs.ts` 模块注释明确 —— 本版**没有「实例 → 平台」的身份通道**(唯一那份 `.overlay-device.json` 是**控制面代签**的)。
- ⇒ **判定**:提法**成立**(客户端侧跑 agent CLI、把结果回给平台),但 **今天没有落点,且不是"写个插件"就能补** —— 至少要新增:① 客户端侧进程的起与守护(引导层第 6 件同类物)② 一条稳定的实例→平台身份通路 ③ 与 `sessions.kind`(值域只有 `browser|desktop`)/`overlay_devices.kind`(含 `instance`)的身份对齐。
- ⇒ **登记为缺口,本件不含设计。**
### §2.5 覆盖网络 —— 三个门都在插件之前
入网要过三个门,插件**一门也过不了**:
① **入网**(三级链:env > 缓存目录 > 内置种子;**签名不对 ⇒ 失败关闭**;凭据**不含密钥本体**)
② **准入**(relay `dialers: Map<network, Set<hostId>>` **默认拒绝** ⇒ 结构性隔离)
③ **通路**(443 全可用,⛔ 不用开端口)
⇒ 三件都在"这台机还没成为节点"的时刻。插件的用武之地在**门内**:台账、设备授权引导、连接状态。
### §2.6 IM 对话 —— 插件纵深最深的一条 🔴 含一处更正
**🔴 更正(本件实测,推翻 09-25 的一条旧结论)**
| | 说法 | 现状 |
|---|---|---|
| 旧 | 09-25 审计:「扩展点注册/入向回调/出向投递**三者无跨进程通路**」⇒ 当时最大的接入缺口 | ❌ **已被推翻** |
| 新 | `im.ts:333` 起「跨进程桥:登记 / 注销 / 出向」+ `im.ts:475` 起「**反向通道(平台 → 插件)· 2026-09-26**」:`register` / `unregister` / `out/frame` / `out/message` + `bridge/pull` 长轮询 + `bridge/result` 回投;身份走**实例令牌 + 包名头**,⛔ 缺头/过期 ⇒ **401 且什么都不做**(fail-closed) | ✅ **通路已成立** ⇒ 插件接入**四个面齐全**(装配 / 运行 / 取数 / 回调) |
**残留(如实登记,两条)**
1. ⚠️ **不校验"该插件是否真在该实例启用"**(`im.ts:305-307` 自述:与 `/api/im/data/*` 同源,现靠**逐次审计兜底**)⇒ 补法 = **每插件令牌**,属投放线。
2. ⚠️ **一条实例只该有一条常驻长轮询**(`im.ts:482` 原文:⛔ 别每插件一条)—— 直接关系**连接额度**。
⇒ 所以「IM 对话」是**唯一一条插件能深度承下来**的特性;但它承的是**界面 + 收发 + 出向 + 回调**,**不是**权威库与代答判据。
---
## §3 为什么承不了 —— 总纲 + 六件逐项拆解
### §3.0 不是六条难题,是**同一个循环依赖的六个切面**
> **插件是「实例内的东西」;这六件事全是「让实例 / 平台 / 身份 / 网络存在起来」的事。**
> 用「存在的东西」去制造「它自己的存在条件」= **循环依赖**。
> ⇒ 必须有一个**在环外、先于插件**的存在 —— 那就是引导层。**不是"我们不想做",是"先有鸡还是先有蛋"。**
**三条判据**(可复用的检查单:问"这件事能不能插件化"时套这三条)
| 判据 | 问什么 | 命中 ⇒ |
|---|---|---|
| ① **时序** | 这件事要发生在插件被装进去**之前**吗? | 引导层 |
| ② **身份** | 需要**比插件更高的执行身份**(root / setuid / 内核网络栈)吗? | 引导层 |
| ③ **存活** | 要在**插件不存在的时候**照常发生吗?(更新 / 守护 / 自动处理) | 引导层 |
**⭐ 一条"分水岭"句式**(下面逐件都套它)
> **插件做的是「决定要什么 + 说出来」;引导层做的是「把那件事真的实现」。**
### §3.0.1 三条物理约束(总)
| # | 约束 | 出处 / 实测 |
|---|---|---|
| 1 | **插件跑在实例内** ⇒ 实例没起,插件不存在 | 私有模型凭据只有实例内可用 ⇒ 插件必须在实例内;于是"起实例"永远在插件之前 |
| 2 | **实例的家 = 那台机的绝对路径** ⇒ 隔着网络改不了 | `plugin-assembly.ts` 头:「Manager 隔着网络改不了(写了盘上一个不存在路径,**静默空操作**)」;`user-fs.ts` 同源 |
| 3 | **系统级能力不在插件权限内** | uid / cgroup `MemoryHigh`+`MemoryMax` / 防火墙 / 端口区间(`findFreePortInRange`:撞号会**打到别人的实例**)/ systemd scope —— 全是 OS 层,插件进程碰不到 |
⇒ 这三条决定了:**插件是"应用",引导层是"操作系统"。** 想要应用去干操作系统的活,只有两种结局:干不了,或者悄悄干坏。
### §3.1 起实例(systemd scope)—— ❌
**它成立的前提(四条,缺一不可)**
1. 先算出该用户的 **uid**(`isolation.ts hashUid`;新用户用 `baseUid + row_id`,无碰撞)
2. **以该 uid 起进程**:`orchestrator.ts:1221` → `setpriv --reuid <uid> --regid <uid> --clear-groups <cmd>`
3. **套 cgroup 与 scope**:`orchestrator.ts:1242-1249` → `systemd-run --scope --unit <unit> -p MemoryHigh=…M -p MemoryMax=…M`,产出 `dsh-<uid>-<hash>.scope`;且 **scope 名里的 uid 必须等于 argv 里的 `--reuid`**(`:259-274` 交叉验证)
4. **往那台机的 home 铺 profile 树**(`plugin-assembly.ts` 头:隔网改=静默空操作)
**插件为什么不满足**
插件**自己就活在这个 scope 里**(`:1030` 原文:`Spawn the child inside a bwrap sandbox (mount/pid isolation) with setpriv …`,外面再套 `systemd-run` scope)。要它去创建另一个 scope,等于**要它站在自己的容器外面**。而这四条前提**全是 root / setuid 级动作**,插件权限**只收窄**(R5 硬红线)⇒ 一条也做不到。
**✅ 插件那一半**:请求 + 显示(`POST /api/dsh/launch` 130 / `restart` 152 / `stop` 177、`GET /api/dsh/status` 205)
**分水岭**:**谁创建那个 scope**
#### ❓ 追问「插件代码不能起实例吗?」—— 四条路逐个否掉
| 路 | 能不能 | 一句话 |
|---|---|---|
| **A 插件自己创建 systemd scope** | ❌ | 四重门(下) |
| **B 插件自己 `spawn` 一个 node 进程** | ⚠️ **能起"进程",起不出"实例"** | 六个必要条件零满足(下) |
| **C 插件调平台 HTTP 让它起** | ⚠️ **形态成立,今天没通路** | 两套凭据不通用(下)⭐ 唯一真能补的口子 |
| **D 插件改自己的 profile 让 dsh 自己重启** | ❌ | 那只改变"下一个实例长什么样",不产生"下一个实例" |
**A 的四重门**(全部为实测事实,非推测)
1. **看不到 systemd** —— 实例被 `--unshare-pid`(`orchestrator.ts:1219`)丢进**独立 PID 命令空间** ⇒ 插件**连宿主上的 systemd 都看不见**,`systemd-run` 无从谈起
2. **不是 root,且一出生就被降权** —— 启动链是 `systemd-run` → `bwrap` → `setpriv --reuid <uid> --regid <uid> --clear-groups`(`:1221`)
3. **换不到别人的 uid** —— 以别的 uid 起进程要 `CAP_SETUID`=root 专属 ⇒ 插件**连"别的用户"这个概念都够不到**
4. **看不见别人用户的家** —— 沙箱里只 `--bind root root`(`:1179`,**它自己**的家);别人的 home 是 `0700` + uid 边界,且 `plugin-assembly.ts` 头已实测"隔网改=**静默空操作**"
**B 的"六个必要条件"** —— 平台语义里的「实例」=下面六样**同时在册**,插件**一样也办不到**
| 必要条件 | 平台怎么给 | 插件能自己办到? |
|---|---|---|
| 独立 uid | `setpriv --reuid`(要 CAP_SETUID) | ❌ |
| 独立 cgroup scope | `systemd-run -p MemoryHigh/MemoryMax/CPUQuota/TasksMax`(`:1241-1248`) | ❌ |
| 专属端口(worker 划的不重叠区间) | `findFreePortInRange(base, span)`(`spawn.ts:79-92`,区间耗尽即抛错) | ❌ 自选端口会**撞号 ⇒ 打到别人的实例**(2026-09-16 实测) |
| 自己的 profile 树 | `plugin-assembly.ts` 写目标 home 的 `package.json` | ❌ |
| 在册(DB 行 + 租约) | 只有 Manager 能写(三层权威:归属 / 租约唯一写者) | ❌ |
| 可被访问(代理路由 / 域名映射) | 平台侧 | ❌ |
⇒ 插件 `spawn('node', …)` **确实能起一个进程**,但那个进程:在**同一个**沙箱里、在**同一个** scope 里(抢**同一份** `MemoryMax` / `TasksMax`,OOM 会**连坐宿主实例**)、**没有专属端口**、**不在册**、**谁也进不去**、父实例一崩一起没。⇒ **它不是"实例",是"孤儿进程"。**
**一句话**:**能起进程 ≠ 能起实例。** 平台的「实例」是一组**同时在册的东西**,不是"一个 node 进程"。
**C 的两套凭据**(这条今天最实际,也最容易被忽略)
- `/api/dsh/launch` 的守卫是 `requireAuth`(`dsh.ts:130`)= **浏览器 session cookie**
- 插件手里能拿到的是 `x-dsh-im-instance-token`(`im/instance-token.ts`)—— 它**就是**为"实例内那个没有浏览器的 node 进程"补的通道,但**只服务 IM**(`imAuth`),且**跨区本期拒绝**、fail-closed
⇒ **两套凭据不通用** ⇒ **今天插件连"请求平台起实例"这一半也走不通**(要新开一条授权面)。⭐ 这条是"起实例"这一项上**唯一真能补的口子**,补法 = 给实例令牌一条**受限面**(只允许操作**自己**那个实例的 launch/restart/stop),⛔ 不给通用控制面权限。
**🔴 压轴:自举(bootstrapping)—— 这一条谁都绕不过**
问一句:**平台上第一个实例是谁起的?**
- 那时平台进程还没跑 ⇒ **没有 HTTP 可请求**
- 那时也**一个实例都没有** ⇒ **更没有插件**
⇒ 「起实例」这个能力**必须存在于插件之外**,否则**整个系统第一次起不来**。与"init 进程不是被某个 app 起的"同一个道理。
**🔴 还有一条:不是"做不到",是「互相排斥」**
「插件能起实例」与「实例是多租户隔离的」,**在物理上不能同时成立**:
- 前者要求插件有**跨 uid 边界**的权力(能替别人起进程、写别人的家)
- 后者**正是靠 uid 边界成立**的
⇒ 插件能起实例的那一刻,**恰好是它不再是"多租户实例"的那一刻**(退化成"你自己本机的一个进程" —— 没有隔离、没有配额、没有在册、没有租约,也就没有"多租户"这个能力)。⇒ 这不是"做不做得到",是**做了等于拆掉多租户**(击穿 R5「权限只收窄」)。
**⇒ 精确边界**:插件能表达**「我要起 / 重启 / 停止**我自己的**实例」**,由平台执行(今天连这条都要先补凭据面)。**"起"这个动作,归引导层。**
⚠️ **适用域收窄(见 §3.11)**:本节六条"不行"是**「在我们这条『插件跑在实例内、无集群身份』的形态下」**。**若改成 K8S + 给实例一把限定 namespace 的 SA + Role,插件确实能自主创建自己的实例** ⇒ 那时准确说法变成「**插件不能自己拿到钥匙**」(而不是"不能起实例")。
### §3.2 起窗口首屏 —— ❌(但插件那一半最大)
**它成立的前提**:一个**进程外**的窗口(用户设备上的浏览器窗口 / 桌面壳窗口)。插件是 **profile 层的 CJS 模块**,由**宿主加载**(`apply` + `inject` 契约)。
**插件为什么不满足**:「创建窗口」是**本机进程**的动作;插件运行在**远端实例进程**里(服务器上的 node 进程),它不在那台设备的窗口系统里,也就没有"开窗"这个动作可发。
⭐ **本条最特殊,要单独理解**:**"插件那一半"其实很大** —— 插件的 client bundle 可以做**首屏里的一切**。⇒ 这一项不是"插件没用",而是**别把「开窗」和「画窗」混成一件事**。
**✅ 插件那一半**:**首屏里的全部内容**
**分水岭**:**谁创建窗口**(画什么归插件)
### §3.3 取身份(入网凭据)—— ❌
**它成立的前提**:身份由**别人签发**,且要**在入网之前**就有。
- `device-grant.ts` **五道门**(顺序即判据):停发闸门 → 用户配额 → 签 grant → 注册表落盘 → 台账 → **relay 密钥表(写最后是刻意的)**
- 首启引导 **三级链**:env > 缓存目录 > 内置种子;**签名不对 ⇒ 失败关闭**
**插件为什么不满足**
1. 插件是"**被装配进来的**";装配要 profile;profile 要实例 home ⇒ **又一次循环依赖**
2. 插件**不是签发者**,也不该拿到密钥本体(凭据设计上**不含密钥本体**)
3. 本版**没有「实例 → 平台」的身份通道**(`user-fs.ts`:唯一那份 `.overlay-device.json` 由**控制面代签**)⇒ 插件连"以实例身份说话"这一句,都得**先由平台替它签**。
**✅ 插件那一半**:入网**引导界面**(申请、显示状态、重试)
**分水岭**:**谁签**
### §3.4 把包装进去(拉取器)—— ❌
**它成立的前提(三条,都是"实例没起时"的事)**
1. 要能在**实例没起的时候也跑**(否则"关着机就不更新")
2. 要写**本机**的共享只读层 `/var/lib/dshs/bundled-plugins`(**root:root 755**)
3. 要**重试 + 留痕**(`worker/agent.ts`:超时解耦 / 动态预算 / 只重试瞬态 / 失败留痕)
**插件为什么不满足**
① 插件只在实例起来后才存在 ⇒ **关着实例就永远不更新**(=§2.1 第 4 条)
② 共享只读层是 **root 属主**,插件是普通 uid 且在沙箱内 ⇒ **写不进去**
③ 装进 profile 的 `node_modules`(`link:` 软链)那一步叫**装配**,要改实例 home 的 `package.json` —— 又是"改别人的家"
**✅ 插件那一半**:感知(有新版本)+ 请求 + 进度 + 失败原因
**分水岭**:**谁搬包**
### §3.5 原生网络守护 —— ❌
**它成立的前提**:改内核网络栈(网卡 / 路由 / 防火墙)+ **常驻**。
**插件为什么不满足**
① 这些是 **CAP_NET_ADMIN / root** 级动作。实测旁证:**平台自己的端口守卫就是 root-only 的 iptables** —— `firewall.ts` 头原文「**Linux + root only**;opt in via `config.portGuard` and **fail loud when enabled on a host that cannot apply it**」
② **沙箱把插件的网络权限收得比想象的紧**:实测有一条 —— **插件被 nft 拒绝连 `127.0.0.0/8`(连它自己 spawn 的 gateway 都连不上),最后改成走 unix socket**(`orchestrator.ts:863`)⇒ **插件连本机回环都不是自由用的**
③ 守护必须在**实例之外**活着(实例没起也要守)
**✅ 插件那一半**:策略与状态(用哪条路 / 通不通 / 延迟)
**分水岭**:**谁碰内核网络栈**
### §3.6 起承载服务(Manager · Worker)—— ❌
**它成立的前提**:Manager / Worker 是**平台进程**(`dshs` / `dshs-worker`)。Manager 有**唯一权威库**、签发凭据、**起实例**;Worker 是本机执行者(`src/worker/` **零 DB 引用**、单向拨入、幂等键、self-fencing)。
**插件为什么不满足**
① **Manager 是引导层本体** —— 实例由它起 ⇒ 插件**装不进去**(自己把自己装进自己里)
② **判决者不能由被判决方自选** —— `lease.ts` 三条不变量 + `schema.ts` v7 原子 CAS + `epoch` fencing;双写 ⇒ 脑裂 ⇒ **会话日志 append 冲突 = 数据损坏**
③ Worker 要系统级能力 ⇒ **与 §3.1 同因**
**✅ 插件那一半**:角色选择界面 + 申请(§14 三档:用户设备 ✅ / 轻节点 ✅ / 重节点 ❌)
**分水岭**:**谁跑服务**
### §3.7 六件汇总表
| 六件 | 前提卡在哪 | ✅ 插件那一半 | 分水岭(执行半归谁) |
|---|---|---|---|
| 起实例 | root/setuid + systemd scope | 请求 + 状态 | 谁创建 scope |
| 起窗口首屏 | 本机窗口系统 | **首屏里的全部内容** | 谁创建窗口 |
| 取身份 | 由控制面签发 | 入网引导界面 | 谁签 |
| 把包装进去 | 实例没起也要跑 + root 属主共享层 | 感知 + 请求 + 进度 | 谁搬包 |
| 原生网络守护 | CAP_NET_ADMIN / root | 策略 + 状态 | 谁碰内核网络栈 |
| 起承载服务 | 平台进程本体 + 判决者唯一 | 选角色 + 申请 | 谁跑服务 |
**⭐ 一条必须说清(消解真实顾虑)**
> 六件"不行"**不等于"用户要多装两样东西"**。引导层可以做成**同一个安装包的另一半** —— 用户**只装一次**,装出来的就是"壳 + 引导层 + 插件"。
> ⇒ **"分两层"是我们内部的实现分工,不是用户的操作负担。** 真正要你拍板的仍是 **P1**。
**⚠️ 一条自我约束(别把话说满)**
本节六条的"不行",**强度并不相同**:
- **形式上就不可能**(换形态也一样):**§3.1 起实例**、**§3.6 起承载服务**(循环依赖 / 判决者唯一)
- **在当代 Linux 上不可能**(需要 root 级动作):**§3.3 取身份**、**§3.4 把包装进去**、**§3.5 原生网络守护**
- **是架构分工,不是能力问题**:**§3.2 起窗口首屏**(画窗归我们,开窗归壳)
⇒ 后五条**理论上**会变 —— 如果哪天形态变成"插件被装进一个**本机特权宿主**里"。但那时它已经**不是"插件"了**(它变成了引导层的一部分)。⇒ **本节的"不行"准确表述是:「在我们这条「插件跑在实例内」的形态下不行」**,⛔ 不说成"永远不行"。
---
### §3.8 评估用户方案:「第一个实例 = DSH 主体,加载基础插件后选择成为服务节点」
**用户原话**:「第一个实例是 dsh 主体的,加载这个基础插件后 选择成为服务节点,设置管理员账号,配置数据库后,提供多租户能力,本质是在服务上开多个相互隔离的 dsh 实例服务,这个看起来是成立的」
#### §3.8.0 一句话判定
**七条里六条成立,唯一要改的是一个词:那个"基础插件"不是插件,是引导层。**
分歧**不在架构,在权限**:**能不能拿到 root / 系统级能力**。
- 能 ⇒ **成立**(它就是引导层)
- 不能 ⇒ **四重门照旧**(§3.1)
#### §3.8.1 逐条对
| 用户的说法 | 判定 | 说明 |
|---|---|---|
| ① **第一个实例 = DSH 主体** | ✅ **成立,而且这就是"自举"的答案** | 第一个**不由任何人"起"** —— 管理员 / 安装脚本直接启动它。⇒ 与 §3.1 的「压轴·自举」**不矛盾,正是它的解**:「起实例」的能力确实在插件之外(=**管理员本人 + 安装脚本**) |
| ② 加载基础插件 | ⚠️ **成立,但名字要改** | 见 §3.8.2 |
| ③ 选择成为服务节点 | ✅ 成立 | 此时"服务节点"**已经在那儿了** ⇒ 这是**启用 / 初始化**,不是**创建** |
| ④ 设置管理员账号 | ✅ 成立 ⚠️ **但今天正是缺口** | =定稿 `02-架构设计/覆盖网络-顶层架构全貌.md` **§5.3 路径 A「新服务器首启」**:显示首管理员设置页(**一次性**,建完即永久关闭)。**定稿原文:⚠️ 今天只有 CLI,无 Web 通路**(缺口 G3/G4 同源) |
| ⑤ 配置数据库 | ✅ 成立 | PG + drop-in;一次性动作 |
| ⑥ 提供多租户能力 | ✅ 成立 —— **但这不是新方案,是现状** | 见 §3.8.3 |
| ⑦ **本质 = 在服务上开多个相互隔离的 dsh 实例** | ✅ **准确** | 隔离是**四层叠出来**的:uid + cgroup(`MemoryHigh`/`MemoryMax`/`CPUQuota`/`TasksMax`)+ bwrap(mount / PID 命名空间)+ 每 worker 的端口区间 ⇒ **用户对本质的理解是对的** |
#### §3.8.2 唯一要改的那个词:「基础插件」≠ 插件
**官方 DSH 的插件机制里,没有"以 root 运行"这个位置**:
- 插件是 profile 层的**依赖**(`plugin-assembly.ts`:装配 = 往 profile 的 `package.json` 写 `link:`)
- **加载它的宿主进程本身就被降权** —— `setpriv --reuid <uid> --regid <uid> --clear-groups`(`orchestrator.ts:1221`)+ bwrap(`:1219`)
⇒ **宿主不是 root ⇒ 宿主里的插件永远不可能是 root。** ⇒ "用插件做多租户"**在权限上从一开始就不成立**,与我们怎么写代码无关。
⇒ 两个位置必须分开命名,否则永远吵不清:
| | **业务插件** | **引导层(宿主扩展)** |
|---|---|---|
| 谁安装 | 平台装配 | **管理员本人**(安装包 / setup) |
| 跑在哪 | 用户实例进程内 | 机器上,root / 带能力 |
| 权限 | **只收窄**(沙箱内) | 系统级 |
| 今天叫什么 | mcn-suite / IM 插件 | **`dshs` / `dshs-worker`** |
| 能开隔离实例 | ❌ | ✅ |
⇒ 用户脑中那个"基础插件"= **右栏**。它确实是"装在官方 DSH 上的一层",但**不是 R2 说的那种插件**。
#### §3.8.3 「提供多租户能力」今天已经成立 —— 所以方案的真价值不在⑥
现状:47 上跑着 `dshs`(Manager,127.0.0.1:3080)+ `dshs-worker`(19100),实例是**独立 systemd scope**。
⇒ **"服务节点 + 多租户"今天就有,而且形状与用户描述的一致。**
⚠️ **但要说清"多租户能力"的载荷量**:它不是"插件顺带给的一个功能",**它就是整个平台本体** —— `supervisor/`(12 文件,含 orchestrator / lease / plugin-assembly / crash-policy)+ `web/routes/`(15 文件)+ `db/`(10)+ `net/relay/` + `im/`(12)+ `fs/`。⇒ 把它"装进第一个实例"= **把整个平台装进一个实例**,而不是"加一个小插件"。
⇒ **所以这个提法的真价值在另一件事**:**把引导层从"独立进程"改成"寄居在官方 DSH 里的一层"**(用户只装一次)。现状与它的**真正差别**是:今天 Manager 是**我们自己的程序**,不是 DSH 实例。
#### §3.8.4 "寄居"该不该做 —— 收益 vs 四条代价
**收益**:① 用户**只装一次**(壳 + 引导层 + 插件)② 引导层与 DSH 共用依赖 / 版本 ③ "第一个实例"天然就是宿主。
**代价(四条,如实标)**
1. 🔴 **可靠性降级** —— 引导层与 DSH **同生共死** ⇒ DSH 崩 = **所有实例没人管**。今天 `dshs` 与实例是**分离**的(**双向**都不互相牵连)⇒ 这一条是**净变差**(命中 R11),不能藏进交付里
2. 🔴 **升级耦合** —— 升级 DSH 会牵动引导层,而 **R1 明令不自动升级 dsh** ⇒ 升级动作的危险等级明显上升
3. 🔴 **权限要么混、要么分** —— 引导层要 root,DSH 主体通常以普通用户跑 ⇒ ① DSH 提权(危险,**会推翻现有的降权 + 沙箱设计**)② 引导层仍是独立进程
4. 🔴 **"实例没起也要活"的那几件塞不进去** —— 拉包、网络守护、自动更新(§2.1 第 3 / 4 条)在官方 DSH 的进程模型里**没有位置**
⇒ **建议(可推翻)**:**同一个安装包,两个进程** —— 安装脚本一次装好「官方 DSH(薄壳 / 界面)+ 引导层(以服务形式、root 起)」。
⇒ **用户视角"只装一次",架构上"权限分层不混"。** 这正是 §3.7 那条「引导层 = 同一安装包的另一半」的强化 —— 用户这个提法把它从"我们的说法"变成了**可实现的安装形态**。
#### §3.8.5 一个退化形态必须说清(免得以后争论)
如果**单机、单用户、全部同 uid**(放弃 uid 隔离):
⇒ "开多个 DSH 实例"退化成"多开几个**同 uid** 的 DSH 进程"(分 HOME、分端口)—— **插件确实起得动**(同 uid 可写自己目录、可 spawn、可挑端口)。
⇒ **但那不是"多租户"**:同 uid ⇒ **没有租户边界**,一个租户的会话能读另一个租户的文件。
⇒ 一句话:**"能起实例" 与 "多租户隔离" 在同一个进程身份下不能同时成立** —— 这就是 §3.1「互相排斥」那条的具体形态。
#### §3.8.6 §3.8 结论
**用户方案成立,而且比"插件能承一切"更准确。** 只需两处调整:
1. 把"基础插件"改称 **引导层 / 宿主扩展**(它需要系统级权限)
2. 接受**它是第二个进程**(可以同包安装、一次装好,但不要塞进同一个进程)
⇒ **本质是"同一个安装包,两个进程",不是"一个插件"。真正待拍板的仍是 P1。**
---
### §3.9 评估「把 DSH 实例当作服务器的系统,多租户(含管理员自己的 AI1net 实例)由插件创建 —— DSH 系统里嵌套的多租户 DSH 平台」
**用户原话**:「把现在的dsh实例当作是服务器的系统,多租户包括管理员的AI1net实例是插件创建的这样看呢,相当于dsh系统里面嵌套的 多租户dsh平台」
#### §3.9.0 一句话判定
**"嵌套"不成立;但你要的东西(管理员也有一个自己的 AI1net 实例)今天就是"并列",不需要嵌套。**
最要紧的一句:**「把 DSH 当系统看」≠「它就有系统的权限」。** 系统与应用的差别**不是名字,是特权层** —— 把 DSH 实例改名成"系统",它不会因此长出内核,也不会因此看得见 systemd(§3.1 四重门原样复现)。
#### §3.9.1 先分清两张图:嵌套 vs 并列
| | **嵌套(本提法)** | **并列(今天的形态)** |
|---|---|---|
| 结构 | 外层 DSH(当"系统")→ 插件 → 内层 N 个 DSH | 引导层 → N 个 DSH(**其中一个属于管理员**) |
| "外层"是谁 | 一个 DSH 实例 | **不是 DSH**:是引导层(`dshs` / `dshs-worker`) |
| 管理员实例 | 由插件"创建"出来 | **与其它实例同级、同一套机制**,区别只在 DB 里 `role='admin'` |
| 今天有没有 | ❌ 没有 | ✅ **已经是这样** |
#### §3.9.2 为什么"嵌套"不成立(三条)
① **"当系统看"不产生特权** —— 外层若是普通 DSH 实例,它**自己就在 bwrap 里、被 `setpriv` 降权、看不见 systemd**(`orchestrator.ts:1219/1221`)⇒ 名字改成"系统"不会让它长出内核。
② **要做成"系统",它必须已经是特权层** ⇒ 那就落回 §3.8 的结论:**那是引导层,不是插件**。
③ 🔴 **以 root 跑 DSH + 加载插件 = 把安全模型倒过来** —— 内层靠 uid / bwrap 隔离,而外层是 **root** 且加载**可替换的第三方插件** ⇒ 任何一个越权插件就能开任意 uid 的实例、读任意租户的家 ⇒ **多租户隔离退化成"自愿的"**。这与现有"降权 + 沙箱"设计**直接冲突**。
#### §3.9.3 "嵌套"的三笔实测代价(用本仓现成数字,不估算)
1. 🔴 **先白吃一份基座** —— `orchestrator.ts:1229` 原文实测:**dsh 自身私有内存稳态 106~117 MiB、峰值约 145 MiB**(=**基座成本**,非用户行为);而宿主实机**只有 1870 MiB**(`:1233` 原文:并发由 2 核 / 1870 MiB 与实时 available 决定)。⇒ 嵌套 = **外层先扣一份**(且外层要跑平台逻辑,占用**必然显著大于 145 MiB**),内层每租户再各扣一份 ⇒ **承载能力净下降**(命中 **R11**)。
2. 🔴 **造出两个真相** —— 外层 DSH 自己也有"会话 / 实例 / 配置"的概念 ⇒ 嵌套后**外层自认宿主,内层平台也自认宿主** ⇒ uid / 端口 / 会话 / 账本**四套东西叠加**。而现有设计的核心不变量是**判决者唯一**(`lease.ts` 三条不变量 + `schema.ts` v7 原子 CAS + `epoch` fencing;双写 ⇒ 脑裂 ⇒ **会话日志 append 冲突 = 数据损坏**)⇒ **必须二选一**。
3. 🔴 **分配权会打架** —— `findFreePortInRange(base, span)` 的前提是**每台 worker 一段不重叠区间**;嵌套后外层也在占端口 ⇒ 区间必须由**唯一一方**划(现状是 Manager)。
#### §3.9.4 你要的效果:**不需要嵌套,只需要"并列 + 角色"** —— 而且今天已经是这样
**实测三条**
- `dsh_instances` 按 **`user_id`** 建(`repo.ts:617` INSERT、`:646` `findUserInstance(userId, role)`、`:915` 集群态同键)
- **没有任何"管理员不给实例"的排除逻辑** —— `grep -rn "role === 'admin'" src/supervisor/ src/web/routes/dsh.ts` = **零命中**
- 管理员的"管理员"身份**只体现在平台侧的数据与授权判断里**:`requireAdmin` 就是一句 **DB 角色判断** —— `middleware/authn.ts:31-38`,`request.user.role !== 'admin'` ⇒ 403
⇒ **今天管理员与普通租户跑的是同一个东西**,管理员特权**不在进程权限里**。
⇒ **这是刻意的,也是必须的**:管理员实例**不能比别的实例多一份特权** —— 否则**它一旦被攻破=全网**。⇒ 你要的"管理员也有自己的 AI1net 实例"**今天就有**,而且是**同级并列**。
⇒ **精确表述**:**不是"DSH 里嵌 DSH",是"引导层下面并列着 N 个 DSH 实例,其中第一个属于管理员"。**
#### §3.9.5 那"嵌套"有没有成立的位置?—— 有,但隔离强度会掉到零层
**唯一能叫"嵌套"的形态** = 在**同一个 DSH 里**做多会话 / 多工作区(DSH 自己的权限模型)。但按实测:
- DSH 的会话隔离粒度是**会话**,不是**租户**(`sessions` 表按 `user_id` + `workspaces`;`sessions.kind` 值域只有 `browser|desktop`)
- ⇒ 用它做租户隔离,**没有 uid、没有 cgroup、没有 bwrap** —— 三层全没了
🔴 **而且这条是"往回退"** —— 现成的决定性证据:`orchestrator.ts:823-827` 原文
> 「本机内核 5.10 无 Landlock、bwrap 后端在平台容器内探测失败 → 内置默认 `workspace-write` 会 fail-closed 拒绝执行任何 shell(P0)。改 `danger-full-access` = **不在实例内再叠加沙箱**,**隔离交由平台容器(bwrap + uid + cgroup)负责**」
> 对应代码:`DSH_PERMISSION_MODE: process.env.DSH_PERMISSION_MODE ?? 'danger-full-access'`
⇒ **平台已经明确把"隔离"这件事从 DSH 里搬到了外面**,并**主动关掉了 DSH 自带的沙箱**。⇒ "用 DSH 当系统、在 DSH 里做租户隔离"= **退到一个平台主动放弃的层**。
#### §3.9.6 §3.9 结论
| 提法 | 判定 |
|---|---|
| "DSH 当系统 + 插件建多租户" | ❌ 不成立(**"当系统看"不产生特权**;要特权就成引导层;以 root 跑宿主会把安全模型倒过来) |
| "DSH 实例里嵌多租户 DSH" | ❌ 不成立(**三笔代价**:基座重复扣 / 两个真相 / 端口与 uid 分配权打架) |
| **"管理员也有一个自己的 AI1net 实例"** | ✅ **今天就是,而且是同级并列**(`dsh_instances` 按 `user_id`;`requireAdmin` 只是 DB 角色判断) |
| "DSH 内做多租户" | ⚠️ 隔离强度**从四层降到零层**,且是平台**主动放弃**过的那一层 |
⇒ **现状与你要的形态其实已经是**「引导层 → N 个并列 DSH 实例(首个属管理员)」。差别只在:**引导层今天是我们自己的程序(`dshs`),不是官方 DSH。** ⇒ 待拍板的仍是 **P1**(三层形态 + 同包两进程,走不走)。
---
### §3.10 评估「假如用 K8S 的方案做多租户实例,是否可以解决这个问题」
#### §3.10.0 一句话判定
**能解决一部分 —— 而且解决的正是最难自研的那一部分;但「插件不能起实例」这句话在 K8S 下依然成立。**
因为 **K8S 把门槛从「你是不是 root」换成了「你有没有 RBAC 授权」—— 门槛没了,门还在。**
⇒ 它**把 §3.1 的四重门压成一道门**("有没有一个被授权的身份"),但**那一门仍然在插件之外**。
#### §3.10.1 先摆一个事实:**这条路代码里已经被承认了,只差实现**
| 层面 | 状态 | 证据 |
|---|---|---|
| **设计意图** | ✅ **写明** | `config.ts:18-19`:「`local` = single-host child_process (setuid/iptables);**`k8s` = multi-replica control plane spawning per-user DSH Pods via the K8s API**」 |
| 配置面 | ✅ **已预留** | `k8sNamespace` / `dshImage` / `controlPlaneImage` / `imagePullSecret` / `egressCidrs` / `k8sServiceAccount` / `podName`(env `POD_NAME`) |
| **权限载体** | ✅ **已经换过** | `config.ts:125-126` 原文:「`k8sServiceAccount` = **ServiceAccount the orchestrator runs as (k8s mode only)**」⇒ **从「root」变「SA」** |
| 数据库 | ✅ 已切 | `db/adapter.ts`:`local` ⇒ SQLite;`k8s` ⇒ Postgres(要 `DSHS_DB_URL`) |
| egress 收敛 | ✅ 有配置项 | `egressCidrs` 注释:「443 egress whitelist CIDRs for **per-user DSH Pods**(Phase 4 egress 收敛)」 |
| **实现** | 🔴 **零** | `grep "kubernetes\|@k8s\|coreV1\|appsV1\|createNamespacedPod" src/` = **0 命中**;`package.json` 无 k8s 依赖;**`podName` 零消费点**(选主只有位没实现);`proxy.ts:675/745` 里 `deployMode === 'k8s'` 只是传给 `proxyHttp` 的**一个布尔** |
⇒ **所以问题不是"能不能上 K8S",是"现在值不值得把这条线做出来"** —— 它是一整条线:控制面选主 + 执行体从 systemd 换 Pod + 状态回流 + 设备侧通道。
#### §3.10.2 K8S **真的解决**的四件(对齐 §3.1 的六件)
| §3.1 六件 | K8S 下 | 为什么 |
|---|---|---|
| **① 起实例** | ✅ **真松动** | 执行体从「本机 root + systemd + setuid」换成「**K8S API + Pod**」⇒ **不再需要 root,只需要一个有 RBAC 权限的 ServiceAccount**。配额从手写 cgroup 参数变成 `ResourceQuota` / `LimitRange`;隔离从"uid + bwrap"变成 **namespace + PodSecurityContext + NetworkPolicy**(标准、可审计、少自研) |
| **④ 把包装进去** | ✅ **部分解决** | `initContainer` / `DaemonSet` / `CronJob` 是**原生位置** ⇒ "实例没起也要跑"这件事第一次有了标准载体 |
| **⑤ 原生网络守护** | ✅ **部分解决** | CNI + `NetworkPolicy` + `egressCidrs`(**配置项已有**)接手**集群内**那一半 |
| **⑥ 起承载服务** | ⚠️ **形式变了** | 控制面可做成 `Deployment`(多副本)+ **K8S 原生 `Lease` 对象选主** ⇒ `podName` 那个"只有位没实现"的选主**有了现成载体** |
#### §3.10.3 K8S **不解决**的四件(这才是关键)
1. 🔴 **"谁调 K8S API" 不变** —— 插件若跑在**租户实例内的普通进程**里,它**依旧没有 kubeconfig / SA token / RBAC** ⇒ **一样起不了实例**。⇒ **K8S 把"需要 root"换成"需要 RBAC 授权",但没有取消"需要授权"这件事。** 于是**自举问题一模一样**:**第一个被授权的身份从哪来?** K8S 下也是同一个答案 —— **管理员 / 安装脚本 `apply` 一份 RBAC**。⇒ **§3.9 的"自举"、§3.1 的"四重门",K8S 一条也没绕开,只是换了门牌。**
⚠️ **措辞更正(见 §3.11)**:本条里的"没有…"指的是**没有身份**,⛔ **不是"网络连不上"** —— 沙箱**共享网络命名空间**(全仓唯一一处 `--unshare` 是 `--unshare-pid`,**没有 `--unshare-net`**)⇒ **出网是通的**。详见 **§3.11**。
2. 🔴 **"插件跑在实例内"这条约束不变** —— K8S 里 **Pod 内依旧是普通用户进程**(除非给 `privileged`,那又是把安全模型倒过来)。
3. 🔴 **"两个真相"不解决,只换了对手** —— K8S 帮你**管 Pod**,但**不管你的租户语义**:`dsh_instances` / 租约 / 归属 / 会话账本**还是你的**。⇒ 于是变成「**平台库 vs K8S 实际状态**」两套状态源,而且更难:Pod 会被**驱逐 / 重调度 / 抢占**,你的库得一直接受"我记的和你实际跑的不一样"。
⇒ **唯一干净的解法**:**把租户语义交给 K8S** —— **一个租户一个 namespace**,平台只做"**声明式 controller**"(见 §3.10.6)。
4. 🔴 **设备侧完全不解决** —— 用户的笔记本、浏览器**不是 Pod**,永远在集群外。⇒ 覆盖网络那一半(中继 / 打洞 / 两个网 `ops` / `u:<user>`)**K8S 帮不上**,且与 CNI 是**两套东西**,还得各管各的。
#### §3.10.4 一条必须先摆上桌的代价:**别在一台 2 核机上跑 K8S**
- 宿主实机:**2 核 / 1870 MiB**(`orchestrator.ts:1233` 原文)
- 每个 DSH 实例**基座**就要 **106~145 MiB**(`:1229` 实测)
- 而 `kubelet` + `containerd` + CNI + kube-proxy + 控制面本身,**基座成本会先切掉一大块 1870 MiB**
⇒ **在本机跑 K8S 再跑若干 DSH 实例 = 净变差(命中 R11)。**
⚠️ 但按 2026-09-22 拍的口径「**机器可换**」,这条**不构成否决**,只构成**前提**:**K8S 方案的前提是加机器(或直接用托管 K8S)。**
#### §3.10.5 隔离强度是"升"还是"降"?必须分两栏(⛔ 别笼统说"更强")
| | **现状(uid + bwrap + cgroup)** | **K8S(namespace + RBAC + PodSecurity)** |
|---|---|---|
| **管理面隔离** | 自研,单机够用 | ✅ **更强**(有标准、可审计、RBAC 细到动词) |
| **数据面隔离** | uid + mount/PID ns + iptables 端口守卫 | ⚠️ **默认是"共享内核"** —— Pod 之间靠 namespace / cgroup,**不是内核级隔离**;要真隔离得上 **gVisor / Kata / 独立节点池** ⇒ 复杂度与成本上量级 |
| **跨机能力** | ❌ 没有(§2.2「跨节点迁移未设计」) | ✅ **有**(调度就是迁移) |
⇒ **净判定**:管理面 + 跨机是**净变好**;数据面**取决于上不上强隔离运行时**。
#### §3.10.6 ⭐ 本轮最有价值的一条:**K8S 的声明式模型,与我们说的"分水岭"是同一个分形**
前面那句分水岭:**插件做的是「决定要什么 + 说出来」;引导层做的是「把那件事真的实现」。**
K8S 的模型**逐字同构**:**「写一个 CR(期望态)」vs「controller reconcile(执行)」。**
⇒ **所以 K8S 不解决"插件不能起实例",但它把"插件能做什么"抬高了**:从"只能调我们私有 API 请求"变成"**能写一个标准对象表达期望态**"。
**落法(可推翻):平台 = 一个 Operator + 一个 CRD**
- **CRD**:`DshInstance`(租户实例的期望态)
- **Operator**:watch CRD ⇒ 建 Pod / Service / PVC / NetworkPolicy ⇒ **回报状态**;用 K8S 原生 `Lease` 选主 ⇒ **"判决者唯一"有了天然实现**(只有一个 leader 在 reconcile)
- **租户边界**:**一个租户一个 namespace**(=K8S 原生的租户边界)
- **设备侧**:**不进集群**,仍走平台网关(自研 relay 保留)
⚠️ 但 **写 CR 也需要 RBAC 授权** ⇒ "谁有权写 CR"仍要拍板 —— 而这**正是本件那条新缺口(实例令牌 → 只操作自己实例的受限控制面)在 K8S 下的对应物**:**一个只允许 create/patch 自己那个 `DshInstance` 的 Role**。⇒ **同一个问题的两种实现,本质一致。**
#### §3.10.7 §3.10 结论
| 问题 | K8S 能不能解决 |
|---|---|
| 起实例(谁创建隔离单元) | ✅ **能**(换执行体 + 换权限载体:root ⇒ SA)—— **这是它最大的价值** |
| 配额 / 隔离 / 调度 / 跨机迁移 | ✅ **能,且更标准** |
| 守护与自动更新(实例没起也要跑) | ✅ **部分能**(`DaemonSet` / `initContainer` / `CronJob`) |
| **插件能不能自己起实例** | ❌ **仍然不能**(门槛从 root 换成 RBAC,**门还在**) |
| 自举(第一个被授权的身份) | ❌ 不解决 —— **同一道题换个说法** |
| 租户语义 / 归属 / 账本 | ❌ 不解决,**且会新增"平台库 vs K8S 状态"两套源**(除非 namespace=租户 + Operator 收编) |
| 设备侧互联 | ❌ **完全不解决**(设备不是 Pod) |
| 单机成本 | ⚠️ **净变差**,前提是**加机器** |
⇒ **一句话**:**K8S 能替我们做「操作系统那一半」,替不了「授权那一半」。引导层不会因此消失,只会变薄、变标准** —— 从「本机 root 服务」变成「**集群内被 RBAC 授权的 Operator**」。
---
### §3.11 「插件不能访问网络吗?为什么调不了 K8S API?」—— 一处更正 + 一条重要细化
#### §3.11.0 一句话
**网络这一层通常通;挡住的不是网,是身份。** 我在 §3.10.3 写「没有 kubeconfig / SA token / RBAC」—— **那句说的是「没有身份」,容易被读成「连不上」。此处更正措辞。**
🔴 **而更要紧的是**:**只要引导层在安装时给插件一把限定范围的钥匙,插件就真的能起实例。** ⇒ §3.1「插件不能起实例」在 K8S 下**不再无条件成立**,准确说法要改(见 §3.11.3)。
#### §3.11.1 网络:分三层看,哪层通、哪层不通
| 层 | 状态 | 依据 |
|---|---|---|
| **出网(公网)** | ✅ **通** | bwrap 只 `--unshare-pid`(`orchestrator.ts:1219`),**全仓唯一一处 `--unshare`,没有 `--unshare-net`** ⇒ **网络命名空间共享**;egress 默认 `0.0.0.0/0` |
| **本机回环 `127.0.0.0/8`** | ⚠️ **受限** | 实测:**插件被 nft 拒绝连 `127.0.0.0/8`(连它自己 spawn 的 gateway 都连不上)⇒ 该插件改用 unix socket**(`orchestrator.ts:863`) |
| **私网段**(K8S API 常在这里) | ⚠️ **按配置语义被排除** | `egressCidrs` 注释(`config.ts:122-124`):「443 egress whitelist CIDRs for per-user DSH Pods(Phase 4 egress 收敛);empty = keep the current `0.0.0.0/0`(**except private ranges**)」⇒ **收敛方向恰好会挡住私网里的 API server**。⚠️ 这是**配置语义**,本轮未实测其生效范围 |
⇒ **所以你这一问是对的:不是网络问题。** 大多数网络布局下,**"能连到 API" 是通的**。
#### §3.11.2 真正挡住的:**不是网,是身份**(认证 + 授权两道)
连上了,K8S API server 会依次问两件事:
| 道 | 要什么 | 插件为什么没有 |
|---|---|---|
| **认证**(你是谁) | client cert / **SA token** / OIDC token 之一 | 🔴 **现状连"载体"都没有** —— **SA token 是挂给 Pod 的**(`/var/run/secrets/kubernetes.io/serviceaccount/token`),而我们的实例**是 systemd scope,不是 Pod** ⇒ **没有地方去挂这把 token** |
| **授权**(你能做什么) | Role / ClusterRole + RoleBinding | 🔴 **K8S 的 ServiceAccount 默认零权限** —— 认证过了、没有 RoleBinding 一样 `403`。⇒ **必须由"别人"来绑** |
| 准入(允不允许) | PodSecurity / admission webhook | 另算 |
⇒ **所以现象是 `401 / 403`,不是"连不上"。** ⇒ **K8S 的门是身份门,不是网络门。**
#### §3.11.3 ⭐ 这一问带出的重要细化:**给了钥匙,插件真的能起实例**
**做法**:实例以 **Pod** 形态跑,挂一个 **ServiceAccount**,绑一个 **Role** —— 只允许它在**自己那个 namespace** 里 create / patch **自己那个 `DshInstance`**(或 Pod)。
⇒ 这时**插件是真的在"调用 K8S API 起实例"**,中间**没有我们的私有 API 代理**。
⇒ **所以 §3.1 那句「插件不能起实例」在 K8S 下不再无条件成立。** 准确说法要改成:
> **插件不能「自己获得权限」;但只要引导层在安装时给它一把限定范围的钥匙,它就能在范围内自主操作。**
> ⇒ 分水岭从「**谁执行**」细化为「**谁定义权限范围**」。
⚠️ **这不推翻更早的结论,只是把话说准**:
- **「说出来」vs「实现」仍成立** —— 插件写期望态(CR / Pod),**真正把它跑起来的是 K8S**(api-server → scheduler → kubelet)⇒ §3.10.6 那条"分形"不变
- **自举仍成立** —— 那把钥匙**必须由外部先给**(管理员 / 安装脚本 `apply` 一份 SA + Role + RoleBinding)⇒ **第一个身份仍由人给**
⇒ ⇒ **同一件事:门在,钥匙不在。** 而「插件不能起实例」的**准确版本**是:**「插件不能自己拿到钥匙」**。
#### §3.11.4 为什么现状下没有这把钥匙(三条,都不是"网络")
1. **没有载体** —— 实例是 **systemd scope 不是 Pod** ⇒ **没有地方挂 SA token**
2. **现有的那把钥匙不是这把** —— `x-dsh-im-instance-token`(`im/instance-token.ts`)是**平台自研的内存随机串**,与 K8S 完全无关,且**只服务 IM**(`imAuth`)、跨区拒绝、fail-closed ⇒ **跨不到任何控制面**
3. **就算挂上了,默认权限也是零** ⇒ 回到"必须有人绑 Role"
⇒ **今天缺的是「钥匙」,不是「网线」。**
#### §3.11.5 §3.11 结论(含对 §3.1 适用域的收窄)
| 问题 | 答案 |
|---|---|
| 插件能访问网络吗 | ✅ **能**(沙箱共享网络命名空间;只有本机回环与私网另有收紧) |
| 插件能"连到" K8S API 吗 | ✅ **多数布局下能** —— ⚠️ **§3.10.3 的措辞在本节更正** |
| 插件能"做事"吗 | ❌ **不能**(没 token 载体、默认零权限、要有权必须别人绑) |
| 能不能让插件**真的**起实例 | ✅ **能,而且这是 K8S 的真价值之一** —— 但**钥匙必须由引导层在安装时给**,并**限定到"自己的那个 namespace / 自己的那个实例"** |
| 那 §3.1「插件不能起实例」错了吗 | ⚠️ **在「K8S + 限定 SA」之下不成立**。⇒ 该句的**适用域要写清**:**「在我们这条『插件跑在实例内、无集群身份』的现状形态下不行」**,⛔ 不是"永远不行"(与 §3.7 那条自我约束同源) |
⭐ **对产品方向的正面意义**:**"插件应该能自己起实例"这个直觉,在 K8S 下是对的。** 而且它落地成的正是本件那条缺口 ——「**实例令牌 → 只操作自己实例的受限控制面**」的 K8S 版就是 **限定 namespace 的 SA + Role**。
⇒ **这把 P6 的 A 案多了一条理由**:它让"插件自主"这件事**不必我们自研一套受限令牌**,而是用**标准件**(SA + Role + Namespace)实现。
---
#### §3.11.6 直白回答:**能。** —— 这个功能在插件里怎么写、前置条件是什么
**结论先给**:**在插件里写一段"调 K8S API 起容器"的代码 —— 能做,而且这就是 K8S 下的正常做法。**
我在 §3.10.3 / §3.11.2 说的"不能",指的是**另一件事**:**插件不能自己把"身份"变出来**。这两件事我上一轮混在一段里说了,本节拆开。
**一、整个链条上,真正"必须由外部做好"的只有一环**
| 环节 | 谁做 | 插件能自己搞定吗 |
|---|---|---|
| **网络可达**(连 `kubernetes.default.svc`) | 集群自带(Service + DNS) | ✅ **不用操心** |
| **身份**(token / CA / namespace 三个文件) | **K8S 自动挂给 Pod** | ✅ **不用操心**(前提:实例是 Pod) |
| **授权**(RoleBinding 把 SA 绑到 Role) | **管理员 / 引导层在安装时 `apply` 一次** | ❌ **唯一必须外部的** |
| **镜像**(DSH 那套要能拉) | 安装时配 `dshImage`(**配置项已有**,`config.ts:117`) | ✅ 安装时配好 |
| **调 API 起容器** | **插件自己** | ✅ **这一段就是插件做的** |
⇒ **只有"授权"一环必须在插件之外**;其余要么是集群自带、要么是安装配置、要么就是插件自己干。
⇒ **所以"在插件里做个功能调 K8S API 起容器"完全成立。**
**二、插件里那段代码长什么样**(示意,不是最终实现)
```js
const SA = '/var/run/secrets/kubernetes.io/serviceaccount'
const token = fs.readFileSync(`${SA}/token`, 'utf8') // K8S 挂进来的
const ca = fs.readFileSync(`${SA}/ca.crt`) // 同上
const ns = fs.readFileSync(`${SA}/namespace`, 'utf8') // 同上
await fetch(`https://kubernetes.default.svc/apis/<group>/v1/namespaces/${ns}/dshinstances`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ spec: { tenant: me, image: '…', quota: '…' } }),
})
```
⇒ 这就是**插件调用 K8S API 起实例**。没有魔法,也没有额外障碍。
**三、两条实务提醒(不是"不行",是"注意")**
1. **token 是"读进来的",不是"生出来的"** —— 插件不自己造身份,它读一个**外部塞进来的文件**。⇒ 这就是"钥匙是别人给的"的具体形态。
2. **RBAC 划多细由你定** —— 可以细到"只能在自己那个 namespace 里、只能建名字里带自己标识的对象"。⛔ **但别给 `cluster-admin`** —— 那等于把整个集群交给一个第三方插件。
**四、调哪个资源:直建 Pod vs 建自定义对象(这是唯一真需要选的地方)**
| | **直建 Pod** | **建自定义对象(CR)+ Operator reconcile**(推荐) |
|---|---|---|
| 插件做什么 | 直接 POST 一个 Pod | POST 一个期望态对象(如 `DshInstance`) |
| 能不能跑通 | ✅ **能** | ✅ 能 |
| "没人管"的问题 | ⚠️ Pod 是**一次性的** —— 挂了不会自动重建、被驱逐不会补、升级不会滚动 | ✅ Operator 持续 reconcile,把它维持成期望态 |
| 平台库怎么知道 | ⚠️ 插件要回报、或平台去 watch Pods ⇒ **两个真相** | ✅ Operator 从对象状态同步 ⇒ **平台库有唯一一份人类可读的期望态** |
⇒ **两种都是"插件调 K8S API",区别只在调哪个资源。** 推荐 CR —— 它是唯一能让"期望态只有一份"的写法,正对着 §3.10.3 那条"两个真相"。
**五、那为什么现状做不到?—— 因为承载方式还没换**
- 现状实例是 **systemd scope**,**不是 Pod** ⇒ `/var/run/secrets/kubernetes.io/serviceaccount/` **不存在** ⇒ 插件**没有 token 可读**
- ⇒ 要让上面那套成立,**先得把实例承载从 systemd scope 换成 Pod** —— **这正是 P6 的 A 案**
- ⚠️ **顺带校准一个细节**:代码里的预留是 **一个 namespace 装所有用户的 Pod**(`k8sNamespace` 是单数,注释「K8s namespace for **per-user DSH resources**」,`config.ts:114-115`;且已给文件面预留了 **sidecar** 镜像 `controlPlaneImage`,`:118-119`)。⇒ §3.10.6 我建议的「**一租户一 namespace**」是**更强的隔离方案,不是已有设计**,属可选强化。
**六、一句话收口**
> **链条上真正在插件之外的,只有一份授权绑定(RoleBinding)。而 RBAC 恰好是"一次 `apply`、永久生效、可审计、可回收、可限定范围"的东西** —— 这**正是它比"每台机 root + systemd"好的地方**:**特权被集中进一份声明式文件里。**
> ⇒ **所以在插件里做这个功能,不是"不行",是"要先把承载方式换成 Pod,再给那个 Pod 一份限定范围的授权"。**
---
#### §3.11.7 「为什么要做这条授权?它到底起什么作用」
**先回答"K8S 为什么非要这么设计"**
K8S 的 API server 有两个特殊性质:
1. **它是个网络服务** —— 集群内**任何**一个容器都可能连到它
2. **调它就会改变系统状态** —— 建一个 Pod = 占 CPU/内存/磁盘 + **跑任意镜像里的代码** ⇒ **等价于"在那台机器上执行代码"**
⇒ 所以它必须能回答两个**分开的**问题:
| 问题 | 由什么回答 | 我们的处境 |
|---|---|---|
| **你是谁?**(认证) | client cert / **SA token** / OIDC | ✅ K8S **自动挂给 Pod**,不用我们管 |
| **你能做什么?**(授权) | **RBAC** = Role + **RoleBinding** | 🔴 **必须有人先授** |
**为什么不给"认证过就行"**?因为集群里 Pod 成百上千(探针、日志代理、CNI、ingress、各种 job),**如果"在集群里 = 有权建 Pod"**,那**任何一个有漏洞的容器就能把整台机器占满、起挖矿容器、读别人的 Secret**。
⇒ **K8S 的默认是零信任**:**SA 认证过了,权限是空集。** 这就是 §3.11.2 那句"SA 默认零权限"的由来。
**Role 与 RoleBinding 的分工**(这是这条最容易被跳过的设计)
| 对象 | 一句话 | 类比 |
|---|---|---|
| **Role** | 「**有哪些权限**」——一份**名词表**(能对哪些资源、做哪些动词)。**它本身不生效,只是定义** | 像一份"岗位职责说明" |
| **RoleBinding** | 「**把这些权限发给谁**」——一个**转接头**,把 Role 连到 Subject(User / Group / **ServiceAccount**) | 像"把这份职责指派给某人" |
⇒ **为什么分两个**?因为**复用与审计**:
- 一份 Role 可以绑给很多人 ⇒ **改权限只改一处,全体生效**
- 某人的绑定可以换 ⇒ **不用动 Role**
- **审计时看的是 binding** —— "谁有这个权限"一目了然
⇒ 这与 Linux「文件 rwx 位(权限定义)」vs「用户/组归属(权限归属)」是**同一种思路**。
**在我们这个场景里,它的四条具体作用**
1. 🔴 **它就是"插件能不能建实例"的开关本身** —— 没有它,插件那段 `fetch` 拿到 `403 Forbidden`;有它,**同一段代码**就成功。⇒ **同一份插件代码,行为完全由这条绑定决定。**
2. 🔴 **它把"特权"从"机器"搬到了"一份声明式文件"** —— 现状要起实例必须是**那台机的 root**(特权**跟着机器走**);K8S 下是"有没有一条 binding"(特权**跟着声明走**)。
3. 🔴 **它划出插件的权力边界,而且能划得很细**
4. 🔴 **它把"授权"从代码里挪出去了** —— 插件代码里**不写任何权限判断**(写了也没意义,判断权不在它手里)⇒ 于是 **换授权 = 改一份 yaml 重新 `apply`,不用改插件、不用重启实例、不用重发版本**。
**这段 yaml 长什么样**(带注解)
```yaml
# ① Role = 权限清单:只允许操作「实例」这一类对象
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: dsh-instance-owner, namespace: tenants }
rules:
- apiGroups: ["dshs.example.com"]
resources: ["dshinstances"] # ⛔ 不含 secrets / pods / nodes
verbs: ["create", "get", "patch", "delete"] # ⛔ 不含 deletecollection 等宽动词
---
# ② RoleBinding = 把这份权限发给「我这个实例的 SA」,且只在 tenants 这个 namespace 内生效
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: tenant-a1net-owner, namespace: tenants }
subjects:
- kind: ServiceAccount
name: tenant-a1net
namespace: tenants
roleRef:
kind: Role
name: dsh-instance-owner
```
⇒ 有了这一对,插件**可以做**:建/改/删**自己的**实例。
⇒ **做不到**:看别的租户的实例、读别人的 Secret、动集群节点、建特权容器。**这就是"钥匙"的具体形状。**
**三种给法,差别是量级级的**
| 给法 | 插件调 API 的结果 | 实质 |
|---|---|---|
| **不给** | `403` | 插件只能"看",做不了事 |
| **给一条限定范围的**(上面那种) | ✅ **只能动自己的实例** | **刚好够用** ← 推荐 |
| **给 `cluster-admin`** | ⚠️ 能动整个集群 | ⛔ **等于把集群交给一个第三方插件** |
⚠️ 为什么单挑 `cluster-admin` 说:它等于**把现状的 root 特权原封不动送出去,还多送了一层跨租户能力**(`cluster-admin` 能读**所有** namespace 的 Secret ⇒ **能读别的租户的凭据**)⇒ **这会把多租户从"四层隔离"降成"数据被读走"** —— 比 §3.9 那条"嵌套退化"更严重(进程隔离还在,但**数据已经泄露了**)。
**谁来做这件事?—— 回到"自举"**
- 这条 binding **必须存在在插件运行之前** ⇒ **只能由管理员 / 安装脚本在装的时候 `apply`**
- ⇒ 这就是"自举"在 K8S 下的具体形态:**一把钥匙,装的时候给一次,之后插件自己用。**
⭐ **由此得到一个之前没说透的结论**:
> **在 K8S 形态下,「引导层」会退化成「一份授权 + 一个 reconcile 循环」。**
> 它不再是"每台机上跑着的特权服务",而是**一次 `apply` 的产物 + 一个 Operator**。
⇒ 这正是 §3.10.7 那句"引导层只会变薄、变标准"的**具体含义**。
**一句收口**
> **这条授权的作用,一句话:它把"插件能不能自己开实例"这件事,从"取决于它跑在谁的机器上",变成"取决于有没有人给它签这一张纸"。**
> ⇒ 而签这张纸**只需一次**,之后**可审计、可回收、可收窄,且不用碰代码**。
---
### §3.12 「按新规划,多租户 / 模型管理 / 技能插件管理 / 覆盖网络 / IM 会话能不能装进一个插件?」
> **用户原话(2026-09-26 13:0x)**:「明白这个意思就行 K8S只是一种实例部署方案,按照这个规划 可以把现有多租户,模型管理,技能插件管理,覆盖网络,IM会话等底层框架 放到一个插件中了吗」
#### §3.12.0 一句话判定
🔴 **能 —— 而且你这句话本身就是答案的一半。**
你说「**K8S 只是一种实例部署方案**」,等于把上一轮最难的那一块(**起实例**)从插件里**摘出去**了。摘掉之后,你列的这五块**全部是"业务面",不是"承载面"** ⇒ **能进一个包。**
⚠️ 但有一句必须分清:**「放进一个包」和「全都能自己干完」是两件事。** 五块里每一块都有**一个极小的内核**留在外面,合计**只剩三件**(§3.12.5)。
#### §3.12.1 为什么这次能,而第一轮说不能
| 轮次 | 问的是 | 判定 |
|---|---|---|
| **第一轮** | 插件能否承**整个项目从开始到现在的功能结构** | ⛔ **不能** —— 因为那里面含「承载面 6 件」 |
| **本轮** | 按新规划,五块**底层框架**能否装进一个插件 | ✅ **能** —— 因为这五块**全是使用面** |
⇒ **差别不在插件的能力变了,在问题问得准了**:
> **第一轮问的是「插件能不能让平台*存在*」;本轮问的是「插件能不能承平台的*业务*」。前者不行,后者行。**
⇒ 这也解释了为什么你会觉得"看起来是成立的"(§3.8 你提第一个方案时)—— **直觉是对的,只是当时那个方案里混进了一件不属于插件的事。**
#### §3.12.2 五块逐块
| 块 | 业务逻辑能进包吗 | 🔴 唯一留在外面的那个内核 | 跑在哪 |
|---|---|---|---|
| **多租户** | ✅ 能(租户列表 / 配额 / 成员 / 账单 / 界面) | ⚠️ **"创建隔离实例"这个动作本身** | 🔴 **平台进程** |
| **模型管理** | ✅ 能(配置 / 凭据 / 共享授权 / 界面) | ⚠️ 无额外内核 —— 但**必须跑在实例内** | 实例内 |
| **技能插件管理** | ✅ 能(上传 / 检测 / 建库 / 启用 / 更新 / 界面) | ⚠️ **把包搬进来**(实例没起时也要跑) | 平台 + 实例 |
| **覆盖网络** | ✅ 能(看哪些区 / 选加入 / 跨区可见性 / 界面) | ⚠️ **取身份(入网凭据)** | 主机级(引导层) |
| **IM 会话** | ✅ **已经在跑** —— 唯一「接入四面」齐全的一条 | ⚠️ 无(内核在平台、插件在实例,已走通) | 平台 + 实例 |
⇒ 🔴 **五块里有四块的"最小内核"是同一类东西:要么需要比插件更高的权限,要么要发生在插件存在之前。**(=§3.0.1 那三条判据:**时序 / 身份 / 存活**)
#### §3.12.3 🔴 本轮的第二个核心结论:**这五块不在同一个进程里**
实证(09-25 审计):**插件跑在「用户实例内」,IM 内核跑在「平台进程」⇒ 跨进程。**
| 块 | 天然归谁 | 为什么 |
|---|---|---|
| **多租户** | 🔴 **平台进程** | 要写**平台库**(租户 / 租约 / 归属 / 配额)—— 实例内的插件**连不到平台库、也没有平台权限** |
| **模型管理** | 🔴 **实例内** | 用户自配模型**只有实例内可用**(已实测) |
| **技能插件管理** | 平台(上传/检测/建库/投放)+ 实例(加载/使用) | 两半 |
| **覆盖网络** | 主机级(身份/隧道)+ 实例(界面) | 两半 |
| **IM 会话** | 平台(内核/房间/消息)+ 实例(插件侧) | 两半 |
⇒ 🔴 **所以"放进一个插件"的准确含义是:一个发行包,两个进程各加载它的一部分。**
- ✅ **今天有通路**:**实例内插件 ⇄ 平台进程**(HTTP 服务面 + 反向通道,IM 已走通)
- ⚠️ **今天没通路**:**平台进程不能装插件**(平台不是"实例")⇒ **要在平台侧加的能力,只能加进平台进程本身,⛔ 不能"装进去"**
⚠️ **对本轮问题的影响**:**多租户那一块的管理动作,⛔ 不可能由一个"插件"实现** —— 插件最多做**界面 + 请求**,真正的动作必须加在**平台进程**里。
⇒ **这不破例,它就是分水岭**,只是把分水岭从「插件 vs 引导层」细化成「**插件 vs 平台进程**」。
#### §3.12.4 「一个包」的收益与代价
**✅ 收益三条**(正是 P1 A 案要的东西):
1. **一次安装 = 全套能力**(用户装官方 DSH + 一个包就有全套)
2. **跨端一致**(服务器壳 / 桌面壳装同一个包)
3. **更新只需更一个包**
**⚠️ 代价三条(都要预先知道)**:
1. 🔴 **一插件一库 ⇒ 五块共用一个库。**
实证 —— `schema.ts:132` 原文「**包名 → 库名**:`@dsh-local/trpg-kit` ⇒ `dshs_pl_trpg_kit`」;`PLUGIN_DB_PREFIX = 'dshs_pl_'`(`:90`);库名形状 `dshs_pl_` + 至多 40 位、**总长 ≤ 48**(`:93`/`:100`);库内表再加 `p_<pluginId>_` 前缀(`diff.ts:248`)。
⇒ 落点:**一个包 = 一个 pluginId = 一个库** ⇒ **五块的表全在同一个库里** ⇒ **一次迁移失败牵动全部五块**。
2. 🔴 **一个包 = 一个版本 ⇒ 更新耦合**:改 IM 要把多租户一起重发,⛔ 不能"只更 IM"。
3. ⚠️ **边界会糊**:五块共库共迁移 ⇒ 表归属、迁移顺序、回滚范围都要人工守。
💡 **缓解(可推翻)**:**一个包做发行单元,包内分五个模块、表名分段**(如 `p_ai1net_im_*` / `p_ai1net_tenant_*`),**只留一个迁移入口、按模块分步执行** ⇒ 拿到"一次安装"的好处,同时保住"能单独回滚一块"的余地。
#### §3.12.5 必须留在外面的**只剩三件**(不是六件)
| # | 留在外面的是什么 | 归谁 | 为什么 |
|---|---|---|---|
| **1** | **起实例** | **部署方案**(K8S / systemd,随你选) | 🔴 **你这句话已经解决了** —— "K8S 只是实例部署方案" ⇒ 谁起、怎么起,**不归插件管** |
| **2** | **取身份**(入网凭据:根 / 签名者 / 节点密钥) | **引导层** | **信任根不能自己给自己发**(自举);⚠️ 且现状 **F1.5 未解** |
| **3** | **把插件包装进去** | **引导层** | 插件**不能在不存在时装自己**(自举) |
⇒ 🔴 **六件 → 三件。**
第一轮那六件里,**「起窗口首屏」「原生网络守护」「起承载服务」三件已经并入第 1 件** —— 因为它们本来就是"部署方案该管的"。**你这一句话,把六件里的三件一起划出去了。**
#### §3.12.6 如果真做,工作量在哪
⚠️ **"放进一个包"是重写,不是搬家** —— 这五块今天的代码分布在:
`src/supervisor/*`(承载)· `src/web/routes/*`(管理面)· `src/db/*`(租户与归属)· `src/net/relay/*`(覆盖网络)· `src/im/*`(IM 内核)+ 实例侧。
⇒ 要变成插件形态,等于把这些**重写成插件接口**,⛔ 不是 `git mv`。
✅ **但已经有现成模板**:**IM 那一块是今天唯一「装配 / 运行 / 取数 / 回调」四面齐全的** ⇒ 其余四块**照着 IM 的接入方式做**,是最短的路。⚠️ 残留两条(不校验"插件是否真在该实例启用";一条实例只该有一条常驻长轮询)已在 §4 登记。
#### §3.12.7 收口
> **能进一个包,但要分两半装:一半装进实例(界面 + 业务逻辑),一半长在平台进程里(要写平台库的动作)。**
> **包外面只剩三件:起实例归部署方案,取身份和把包装进去归引导层。**
⭐ **一句话记法**:
> **插件承"要什么",部署方案承"跑起来",引导层承"怎么开始"。**
🔴 **第一轮那个"六件承不了"的结论没有变** —— 变的是:**这六件现在只剩三件,而其中最大的那件(起实例)你已经用一句"K8S 只是部署方案"划到外面去了。**
---
## §4 缺什么(汇总)
| 缺什么 | 属谁 | 状态 |
|---|---|---|
| **agent cli 接入面**(含"实例 → 平台"身份通路) | 新设计 | 🔴 零实现,**本件新具名** |
| **拉取器 + 自动更新的执行者** | 引导层 | 服务器侧已落地(§2.1 情形 3、4 的能力已在);**桌面侧拉取器**已交对接单待实现 |
| **每插件令牌**(治"插件自称身份") | 投放线 + IM 线 | 已具名;本期逐次审计兜底 |
| 🔴 **实例令牌 → 「只操作自己实例」的受限控制面** | 平台侧(新) | **本件新具名**(§3.1 C 条):让插件能表达"起/重启/停止**我自己**的实例"。补法 = 给实例令牌一条**受限面**,⛔ 不给通用控制面权限 |
| 🔴 **平台侧插件宿主**(让同一份包在**平台进程**里也能跑) | 平台侧(新) | **本件新具名**(§3.12.3):现状**平台不是"实例"⇒ 装不了插件** ⇒ 五块里"要写平台库"的那一半(多租户等)**只能加进平台进程本身**。⇒ 没有它,「一个包两半装」的**平台那一半落不了地** |
| **设备读平台插件包权限** | 🔴 **待用户拍板** | 上轮已问,**尚未答复** |
| **插件数据面部署到 47 / 106** | 投放线 | 代码已落地,**未部署** |
| **实例跨节点迁移(承载归属)** | ⛔ 未设计 | 本线只登记,⛔ 不立项 |
| **包签名校验** | 跨线 | 只登记,不设计 |
| **定稿 F1.5**(签名清单序号,治 G1 重放 + G6 撤根) | 覆盖网络线 | **一切前置** —— 任何"下发签名清单"的动作都排在它之后 |
---
## §5 待拍板(⛔ 顺序不可颠倒)
> 下面是累计台账。**P1 是本件判定的直接产物**("插件承什么"的答案取决于"插件规划走不走");**P6 由 §3.10 新增**(承载实现这条轴)。
**P1 —— 底层插件这条路走不走?**
一句话:**要不要按「薄壳(官方 DSH)+ 引导层 + 一个能力包」这个形态做下去**(而不是继续在平台侧堆界面)。
为什么需要你定:它决定后面所有界面代码写到哪个进程里;一旦铺开再改,等于界面全重写。
**A 案(走)**:按三层形态做(优点:用户装官方 DSH + 一个包就有全套能力,跨端一致,更新只需更一个包;缺点:引导层 6 件要单独维护,首装包必须匿名可取,桌面/服务器两条壳要分别实现)。
**B 案(不走)**:维持现状(平台门户 + 实例内各自为政)(优点:不动现有结构,风险最低;缺点:每个端各自实现一套界面,更新只能按端推,用户看到的"能力"始终是拼出来的)。
**我的倾向**:A(可推翻)。
**P2 —— 设备要不要获得「读取平台插件包」的权限?** ⚠️ **上轮已问,尚未答复**(权限面,属红线)
**P3 —— 节点对外提供哪些服务?**(不选就等于节点只能跑实例,不能对外供能)
**P4 —— 跨用户要不要打通?**(不同意 ⇒ 保持结构性隔离,跨用户只能走平台)
**P5 —— 平台要不要做成用户可自建?**(与 P1 不同轴;倾向 A 案分两步:先做选主,「升根」挂起到撤根机制有结论;整体排在 **F1.5** 之后)
**P6 —— 实例的承载走不走容器编排(K8S)?**(与 P1 **正交**,可并行评估;见 §3.10)
一句话:**租户实例是继续用我们自研的「本机 root + systemd + setuid」起,还是改成「K8S API + Pod」。**
为什么需要你定:**它要加机器**(本机 2 核 / 1870 MiB,装不下 K8S 再装实例)⇒ 属**花钱与资源承诺**;同时它决定"隔离靠自研还是靠标准件"、"实例能不能跨机调度"。
**A 案(走 K8S)**:平台做成一个 Operator + 一个 `DshInstance` CRD,一租户一 namespace。
(优点:**起实例不再要 root、只要 RBAC**;配额 / 隔离 / 调度 / 守护全是标准件;**实例可跨机调度** —— 顺手解掉「跨节点迁移未设计」;多副本控制面选主有现成的 `Lease`;**见 §3.11:让"插件自主操作自己的实例"这件事用标准件(限定 namespace 的 SA + Role)就能实现,不必自研受限令牌**。
缺点:**要加机器**;数据面默认共享内核,强隔离要另上运行时;**新增"平台库 vs K8S 状态"两套源**;这条线**代码里实现是零**,控制面选主与执行体都要新做。)
**B 案(不走,维持自研单机)**:继续 `uid + bwrap + cgroup + systemd`。
(优点:**一台 2 核机就能跑**,单机多租户下隔离等级够用,零新增基础设施。
缺点:**不能跨机迁移**;选主只有位没实现;守护与自动更新仍要自研。)
**我的倾向**:**先 B 后 A** —— 现在按 B 把功能跑通(P1 优先),K8S 这条**登记为"承载演进方向"**,等真出现「必须加机器」或「必须跨机调度」的需求再切;⛔ 不建议现在启动。
**不在本线拍板(只指路)**:定稿 §8.1 骨干资格签发权(F4 前必须定,倾向 B 区签 + 根背书)· 定稿 §8.2 根清单几把 / 谁保管(凭据类,必须你定)。
---
## §6 出处
**代码锚点(本仓 `D:/github/dsh_shenxian`,实测)**
`src/supervisor/{orchestrator,crash-policy,plugin-assembly,lease,spawn}.ts`|`src/worker/agent.ts`|`src/web/routes/{dsh,sessions,desktop,skills,admin,auth,im,plugin-data,business-plugins,overlay,overlay-device,overlay-nodes,whitelist}.ts`|`src/im/{agent-bridge,store,ws,hub}.ts`|`src/net/relay/{directory,registry,network,device-grant}.ts`|`src/fs/user-fs.ts`|`src/db/{schema,repo,pg,types}.ts`|`src/config.ts`
**定稿(唯一权威)**:`dsh-server-docs/02-架构设计/覆盖网络-顶层架构全貌.md`(定稿 2026-09-21)
**本线前序成品件**
- `交付物/平台新增功能总盘点与插件化归属-20260926.md`(§3 官方基线 / §9 基础服务插件形态 / §10 ⛔不可进清单 / §12.5 追问链速览 / §16 为什么不行)
- `交付物/基础服务插件-能做不能做总清单与拍板路径-20260926.md`(§3 ✅能做 / §4 ❌不能做 6 件 / §6 拍板路径)
- `交付物/节点与端-互联互通与调用关系-20260926.md`(§1 七类节点端 / §3 联通矩阵 / §4 三条调用链路 / §6 缺口 7 条)
- `交付物/五种角色选择场景-明确件-20260926.md`(对定稿 §5.3 四条入口路径 A–D)
- `交付物/版本管理与包更新机制-执行报告-20260926.md`(treeSha256 判版本 / 通道 / 钉版 / 回滚实测)
---
**结论落一句**:**七项功能的使用面,一个底层插件基本都承得住;七项功能的承载面,一件都承不了 —— 而承载面恰好是"项目能不能跑起来"的那一半。所以答案不是"能"也不是"不能",是"得三层一起"。**