- 变更规模:新增 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/ 知识文件,按口径入库)
80 KiB
全功能承载判定 —— 底层插件能否承整个项目 · 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) |
✅ 通路已成立 ⇒ 插件接入四个面齐全(装配 / 运行 / 取数 / 回调) |
残留(如实登记,两条)
- ⚠️ 不校验"该插件是否真在该实例启用"(
im.ts:305-307自述:与/api/im/data/*同源,现靠逐次审计兜底)⇒ 补法 = 每插件令牌,属投放线。 - ⚠️ 一条实例只该有一条常驻长轮询(
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)—— ❌
它成立的前提(四条,缺一不可)
- 先算出该用户的 uid(
isolation.ts hashUid;新用户用baseUid + row_id,无碰撞) - 以该 uid 起进程:
orchestrator.ts:1221→setpriv --reuid <uid> --regid <uid> --clear-groups <cmd> - 套 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交叉验证) - 往那台机的 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 的四重门(全部为实测事实,非推测)
- 看不到 systemd —— 实例被
--unshare-pid(orchestrator.ts:1219)丢进独立 PID 命令空间 ⇒ 插件连宿主上的 systemd 都看不见,systemd-run无从谈起 - 不是 root,且一出生就被降权 —— 启动链是
systemd-run→bwrap→setpriv --reuid <uid> --regid <uid> --clear-groups(:1221) - 换不到别人的 uid —— 以别的 uid 起进程要
CAP_SETUID=root 专属 ⇒ 插件连"别的用户"这个概念都够不到 - 看不见别人用户的家 —— 沙箱里只
--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 > 缓存目录 > 内置种子;签名不对 ⇒ 失败关闭
插件为什么不满足
- 插件是"被装配进来的";装配要 profile;profile 要实例 home ⇒ 又一次循环依赖
- 插件不是签发者,也不该拿到密钥本体(凭据设计上不含密钥本体)
- 本版没有「实例 → 平台」的身份通道(
user-fs.ts:唯一那份.overlay-device.json由控制面代签)⇒ 插件连"以实例身份说话"这一句,都得先由平台替它签。
✅ 插件那一半:入网引导界面(申请、显示状态、重试) 分水岭:谁签
§3.4 把包装进去(拉取器)—— ❌
它成立的前提(三条,都是"实例没起时"的事)
- 要能在实例没起的时候也跑(否则"关着机就不更新")
- 要写本机的共享只读层
/var/lib/dshs/bundled-plugins(root:root 755) - 要重试 + 留痕(
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 共用依赖 / 版本 ③ "第一个实例"天然就是宿主。
代价(四条,如实标)
- 🔴 可靠性降级 —— 引导层与 DSH 同生共死 ⇒ DSH 崩 = 所有实例没人管。今天
dshs与实例是分离的(双向都不互相牵连)⇒ 这一条是净变差(命中 R11),不能藏进交付里 - 🔴 升级耦合 —— 升级 DSH 会牵动引导层,而 R1 明令不自动升级 dsh ⇒ 升级动作的危险等级明显上升
- 🔴 权限要么混、要么分 —— 引导层要 root,DSH 主体通常以普通用户跑 ⇒ ① DSH 提权(危险,会推翻现有的降权 + 沙箱设计)② 引导层仍是独立进程
- 🔴 "实例没起也要活"的那几件塞不进去 —— 拉包、网络守护、自动更新(§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 结论
用户方案成立,而且比"插件能承一切"更准确。 只需两处调整:
- 把"基础插件"改称 引导层 / 宿主扩展(它需要系统级权限)
- 接受它是第二个进程(可以同包安装、一次装好,但不要塞进同一个进程)
⇒ 本质是"同一个安装包,两个进程",不是"一个插件"。真正待拍板的仍是 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 "嵌套"的三笔实测代价(用本仓现成数字,不估算)
- 🔴 先白吃一份基座 ——
orchestrator.ts:1229原文实测:dsh 自身私有内存稳态 106~117 MiB、峰值约 145 MiB(=基座成本,非用户行为);而宿主实机只有 1870 MiB(:1233原文:并发由 2 核 / 1870 MiB 与实时 available 决定)。⇒ 嵌套 = 外层先扣一份(且外层要跑平台逻辑,占用必然显著大于 145 MiB),内层每租户再各扣一份 ⇒ 承载能力净下降(命中 R11)。 - 🔴 造出两个真相 —— 外层 DSH 自己也有"会话 / 实例 / 配置"的概念 ⇒ 嵌套后外层自认宿主,内层平台也自认宿主 ⇒ uid / 端口 / 会话 / 账本四套东西叠加。而现有设计的核心不变量是判决者唯一(
lease.ts三条不变量 +schema.tsv7 原子 CAS +epochfencing;双写 ⇒ 脑裂 ⇒ 会话日志 append 冲突 = 数据损坏)⇒ 必须二选一。 - 🔴 分配权会打架 ——
findFreePortInRange(base, span)的前提是每台 worker 一段不重叠区间;嵌套后外层也在占端口 ⇒ 区间必须由唯一一方划(现状是 Manager)。
§3.9.4 你要的效果:不需要嵌套,只需要"并列 + 角色" —— 而且今天已经是这样
实测三条
dsh_instances按user_id建(repo.ts:617INSERT、:646findUserInstance(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 不解决的四件(这才是关键)
- 🔴 "谁调 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。 - 🔴 "插件跑在实例内"这条约束不变 —— K8S 里 Pod 内依旧是普通用户进程(除非给
privileged,那又是把安全模型倒过来)。 - 🔴 "两个真相"不解决,只换了对手 —— K8S 帮你管 Pod,但不管你的租户语义:
dsh_instances/ 租约 / 归属 / 会话账本还是你的。⇒ 于是变成「平台库 vs K8S 实际状态」两套状态源,而且更难:Pod 会被驱逐 / 重调度 / 抢占,你的库得一直接受"我记的和你实际跑的不一样"。 ⇒ 唯一干净的解法:把租户语义交给 K8S —— 一个租户一个 namespace,平台只做"声明式 controller"(见 §3.10.6)。 - 🔴 设备侧完全不解决 —— 用户的笔记本、浏览器不是 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 为什么现状下没有这把钥匙(三条,都不是"网络")
- 没有载体 —— 实例是 systemd scope 不是 Pod ⇒ 没有地方挂 SA token
- 现有的那把钥匙不是这把 ——
x-dsh-im-instance-token(im/instance-token.ts)是平台自研的内存随机串,与 K8S 完全无关,且只服务 IM(imAuth)、跨区拒绝、fail-closed ⇒ 跨不到任何控制面 - 就算挂上了,默认权限也是零 ⇒ 回到"必须有人绑 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 起实例。没有魔法,也没有额外障碍。
三、两条实务提醒(不是"不行",是"注意")
- token 是"读进来的",不是"生出来的" —— 插件不自己造身份,它读一个外部塞进来的文件。⇒ 这就是"钥匙是别人给的"的具体形态。
- 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 有两个特殊性质:
- 它是个网络服务 —— 集群内任何一个容器都可能连到它
- 调它就会改变系统状态 —— 建一个 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「用户/组归属(权限归属)」是同一种思路。
在我们这个场景里,它的四条具体作用
- 🔴 它就是"插件能不能建实例"的开关本身 —— 没有它,插件那段
fetch拿到403 Forbidden;有它,同一段代码就成功。⇒ 同一份插件代码,行为完全由这条绑定决定。 - 🔴 它把"特权"从"机器"搬到了"一份声明式文件" —— 现状要起实例必须是那台机的 root(特权跟着机器走);K8S 下是"有没有一条 binding"(特权跟着声明走)。
- 🔴 它划出插件的权力边界,而且能划得很细
- 🔴 它把"授权"从代码里挪出去了 —— 插件代码里不写任何权限判断(写了也没意义,判断权不在它手里)⇒ 于是 换授权 = 改一份 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 案要的东西):
- 一次安装 = 全套能力(用户装官方 DSH + 一个包就有全套)
- 跨端一致(服务器壳 / 桌面壳装同一个包)
- 更新只需更一个包
⚠️ 代价三条(都要预先知道):
- 🔴 一插件一库 ⇒ 五块共用一个库。
实证 ——
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 = 一个库 ⇒ 五块的表全在同一个库里 ⇒ 一次迁移失败牵动全部五块。 - 🔴 一个包 = 一个版本 ⇒ 更新耦合:改 IM 要把多租户一起重发,⛔ 不能"只更 IM"。
- ⚠️ 边界会糊:五块共库共迁移 ⇒ 表归属、迁移顺序、回滚范围都要人工守。
💡 缓解(可推翻):一个包做发行单元,包内分五个模块、表名分段(如 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 判版本 / 通道 / 钉版 / 回滚实测)
结论落一句:七项功能的使用面,一个底层插件基本都承得住;七项功能的承载面,一件都承不了 —— 而承载面恰好是"项目能不能跑起来"的那一半。所以答案不是"能"也不是"不能",是"得三层一起"。