Files
dsh_ai1net_server/交付物/全功能承载判定-底层插件能否承-20260926.md
T
admin c1b5e4d966 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/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

80 KiB
Raw Blame History

全功能承载判定 —— 底层插件能否承整个项目 · 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 起容器"完全成立。

二、插件里那段代码长什么样(示意,不是最终实现)

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 长什么样(带注解)

# ① 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 判版本 / 通道 / 钉版 / 回滚实测)

结论落一句:七项功能的使用面,一个底层插件基本都承得住;七项功能的承载面,一件都承不了 —— 而承载面恰好是"项目能不能跑起来"的那一半。所以答案不是"能"也不是"不能",是"得三层一起"。