Files

185 lines
17 KiB
Markdown
Raw Permalink Normal View History

# S7「第三块:模型管理」· 落地与四轮回执(2026-09-26 · 第 45 棒)
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server` | **会话名**:`插件投放与分库线-45`
> **执行依据**:本线入口 §0 最新行 + §2「🎯 本轮动作」(第 44 棒块)|规格书 `05-交接单/交接单_包规格书_20260926.md` §〇/§五 A3·A4·C|方案 `交付物/功能打包到基础插件-分步实施方案-20260926.md` §3.10.9 + §4
> **本棒只做一件事**:S7(⛔ 不碰 S8、⛔ 不跨 S9 的 P2)
---
## 一、一句话结论
S7 四条判据 **全部落地并逐条取证**——包 **`@dsh-local/ai1net`** 现在**自带模型管理面**:三条精确路由 / 六种方法(`/api/ai1net/models`〔GET·PUT〕· `/models/credentials`〔GET·POST·DELETE〕· `/models/grant`〔POST〕),**明文密钥只走一个入口、读路径根本不存在密钥**,共享模型**默认不共享 + 管理员逐用户授权**,写 home **一律走平台 `UserFs`** 且「直写 fs 的静默空操作」成了**可断言的检出**。
本棒**未抓到产品侧真 bug**;抓到并修掉的是**五处仪器错**(其中三处不修会产出**假绿 / 假红**)——逐条见 §九。
读数:包内门禁 **85 ✅ / 0 ❌(rc=0,S6 时为 60)**|S7 取证编排 **73 ✅ / 0 ❌(rc=0)**|门禁负控 **8/8(7 处改坏全部被门禁抓到)**|四把尺子 **装配端A/端B · 取数 · 回调 · 运行 全 rc=0**|`v-neg all` rc=0。
---
## 二、四条判据逐条回执
| # | §3.10.9 判据原文 | 本棒落地 | 取证 | 结论 |
|---|---|---|---|---|
| S7-1 | 模型配置/凭据/共享授权/界面 进包 ⇒ `v-plugin.sh` 绿 | `lib/models-sdk.mjs`(**新建** 32,634 B)+ `lib/index.js` 三条注册面 + `lib/client.js` 第四张卡 | 装配 端A/端B · 取数 · 回调 · 运行 **全 `rc=0`**;spy ctx 抓到三条路由原文;stub fetch 抓到 **13 条**出向逐条对得上 | ✅ |
| S7-2 | 共享模型走**管理员逐用户授权**(默认未授权)⇒ **负控:未授权用户看不见** | `SHARED_MODEL_POLICY`(`default:'not-granted'` · `grantedBy:'admin-only'` · `scope:'per-user'`)+ `grantShared()` 就地闸 | 策略表三条机验;grant 缺 `asAdmin` / 缺 `userId` / 缺 `enabled` 三条负控;**撤权负控**:管理员撤掉授权 ⇒ 用户自己开关虽开,视图里 `granted=false` | ✅ |
| S7-3 | 写 home 一律走 `UserFs` ⇒ **负控:直写 `fs` ⇒ 能检出"静默空操作"** | `HOME_WRITE_POLICY` + `createHomeWriter()` + 纯函数 `classifyHomeWrite()`;内核**零 `fs` 用法**(门禁扫代码行) | 支撑两支真跑:走 UserFs ⇒ `delivered=true`;**直写 ⇒ `silentNoopDetected=true`**(写成功、回读为空);无 UserFs ⇒ **拒**(⛔ 不退化成本机 fs) | ✅ |
| S7-4 | 验「平台级凭据 ⛔ 不下发客户端」(R5 红线)⇒ 客户端侧**零**平台凭据 | `redactModelConfig()` **白名单净化器**(全包唯一)+ 落台账前的**明文自检**(fail-closed) | 平台**故意**把明文塞进回体 ⇒ 出包体零明文(读配置 / 观测面 / 新增回包三条);明文在包内**只出现在写入那一条出向**上;`stats()` / 台账 / 日志三处零明文 | ✅ |
**完成判据**(§3.10.9 原文):模型配置**在实例内**可见(三条路由挂在本实例,经 `connection.fetch.register` 精确路径)+ **平台级凭据不下发客户端**(读路径结构化无密钥字段)。
---
## 三、三条路由 / 六种方法 → 平台真端点(逐条真发 · stub fetch 记账)
| 包内路由 | 方法 | 平台真端点 | 依据(真实实现) |
|---|---|---|---|
| `/api/ai1net/models` | GET | `GET /api/me/keys` | `src/web/routes/auth.ts:484` |
| `/api/ai1net/models` | PUT〔`role:'shared'`〕 | `POST /api/me/models/shared` | `auth.ts:617`(只动**自己**的偏好) |
| `/api/ai1net/models` | PUT〔`role:'entry-toggle'`〕 | `POST /api/me/keys/<id>/toggle` | `auth.ts:598` |
| `/api/ai1net/models/credentials` | GET | `GET /api/me/keys` | 同上(**只回"是否已配置"**) |
| `/api/ai1net/models/credentials` | POST | `POST /api/me/keys` | `auth.ts:526`(🔴 明文**唯一**一次转交) |
| `/api/ai1net/models/credentials` | DELETE | `DELETE /api/me/keys/<id>` | `auth.ts:664` |
| `/api/ai1net/models/grant` | POST | `POST /api/admin/users/<id>/models/shared` | `admin.ts:107`(`preHandler: requireAdmin`) |
🔴 **三条路径 → 六种方法**:`role` 分流复用同一条路径(与 M5 长轮询、M3「设置自动处理」同一手法)⇒ ⛔ **没有出现第四条路径**(门禁 ①g 第 ③/④ 条断言)。
🔴 **为什么包内读不出密钥**(真实依据,⛔ 不是设计稿):平台 `CREDENTIAL_COLS = 'id, key_name, enabled, updated_at, route, base_url, api, models'`(`src/db/repo.ts:346`)—— **本来就没有密钥列**。包内再过一遍净化,是防"平台日后把 ref 带出来"。
### 该拒就拒:十二条,一律**零平台请求**
| 类别 | 条数 | 例 |
|---|---|---|
| 入参形态闸 | 9 | 新增缺 `name` / 缺 `apiKey`|删除缺 `id` / `id` 穿越(`../`)|`PUT` 的 `role` 非法|授权缺管理员声明 / `asAdmin:false` / 缺 `userId` / `userId` 穿越 |
| 凭据闸(复用 `createHostKit`) | 3 | 出示空凭据(带身份头不带令牌)|借别人的令牌|身份头与用户名不符 |
> 复核命令:`node tmp/p45/run-s7.mjs` → B 段 12 行;每行都同时断言 `reason` 与 `平台请求=0`。
---
## 四、🔴 凭据不出包(S7-4「明文只出现一次」是机验结论)
| 观测面 | 断言 | 结论 |
|---|---|---|
| 读配置出包体 | 平台回体**故意**带 `apiKey`/`secret`/`apiKeyRef` ⇒ 出包体零明文 + `redactions>0` | ✅ |
| 凭据观测面 | 只回 `{id,name,enabled,configured:true,route,models}` —— **连 `apiKey` 字段都没有** | ✅ |
| 新增的回包 | 平台回体**回显**密钥 ⇒ 出包体零明文(包内只挑元数据重造) | ✅ |
| 出向请求 | 含明文的出向**全都是** `POST /api/me/keys`(那唯一一条写入) | ✅ |
| `stats()` / 包内台账 / 包内日志 | 三处序列化后**零明文**;台账只记 `hasKey:true` | ✅ |
| 落台账前自检 | `plaintextBlocked=0`(⛔ 没有任何明文走到过台账;命中即拦下并计数) | ✅ |
---
## 五、🔴 S7-2/S7-3 两条负控(本棒核心)
**S7-2 负控(撤掉授权 ⇒ 立刻拿不到)**:同一份代码、只差授权位——
`sharedModelEnabled:true`(用户自己开着)+ `sharedModelGranted:false`(管理员撤了)⇒ 视图里 `granted=false`;把授权位改回 `true` ⇒ `granted=true`。⇒ **"能不能用"确实由授权决定**,而不是由界面或偏好决定。
**S7-3 负控(直写 fs ⇒ 检出静默空操作)**:
```
走 UserFs :{"via":"userfs","ok":true,"delivered":true,"silentNoop":false}
直写 fs :{"via":"local-fs","silentNoopDetected":true,"ok":true,"delivered":false,"silentNoop":true,"reason":"readback-empty"}
无 UserFs :{"ok":false,"reason":"userfs-unavailable"} ← ⛔ 不退化成本机 fs
```
判据不是"写没报错",是**回读**(且回读走权威路径 `UserFs`)—— §4-D4 的症状正是"写入成功返回、内容根本没到"。
---
## 六、🔴 真实 vs 建模(逐条标注 · ⛔ 不许把建模当"已通")
| 面 | 本棒是**真实**还是**建模** | 说明 |
|---|---|---|
| 包内三条路由的实现与注册 | **真实** | `lib/index.js` 真装配 + 真 `apply(ctx)`(spy ctx 抓到三条路由原文与 methods) |
| 包内 → 平台的出向请求 | **真实**(HTTP 层) | 三条头(包名 / 每插件令牌 / 实例令牌)+ `body` 真的 `JSON.stringify`;stub fetch 逐条记账 |
| **平台侧收到的效果** | ⛔ **建模** | 平台端由 stub fetch / spy 代替;**未对真机投放**(属投放线) |
| 平台端点**字面量与形状** | **真实(照抄)** | 逐条取自 `auth.ts` / `admin.ts` 的实际行(见 §三表最后一列) |
| `/models/grant` 的**管理员权威闸** | ⛔ **平台侧未在本棒真跑** | 真实判据 = 平台 `requireAdmin`(`admin.ts:109`)。包内那道 `asAdmin` 只是**误调用闸**,⛔ **不许当成安全边界**、⛔ 不许写"平台侧管理员闸已上线" |
| 写 home 走 `UserFs` | ⛔ **建模**(注入式假 `UserFs`) | 真实平台侧 `app.userFs`(`auth.ts:646-652` 同款口径)未在本棒真跑;本棒证的是**机制与检出**成立 |
| 客户端第四张卡片 | **机械判据真实 / 视觉效果未取到** | 门禁覆盖封包契约(lazy-CJS · 零 `exports.default` · `type=password` · 提交后清空);**真壳截图未取到**(§八) |
| 「共享模型两列两个主体」 | **真实(照抄口径)** | `shared_model_granted`(管理员授权)⇄ `shared_model_enabled`(用户偏好),两者是**与**关系(`auth.ts:608-624` + `admin.ts:92-105` 原文口径) |
⇒ ⛔ **不能断言**「真机投放已通 / 平台侧管理员闸已上线 / 客户端卡片在真壳里长这样」。
---
## 七、§4 避坑总表对表(本棒相关类目)
| 类目 | 本棒处置 |
|---|---|
| **B2** 桌面端只认精确路径 | 三条逐字冻结、⛔ 无前缀匹配;门禁把三条 + **六种方法**与规格书 §五 A3 逐字比对 |
| **B3** 平台不是实例 | 「写平台库」的动作⛔ 不进包:授权只发请求,**落库权威在平台**(回体标 `executorInPackage:false`) |
| **D3** 列级 `maxBytes` 唯一把关人 = 插件侧 | 本棒**未改** `dsh.data.yaml` ⇒ `DECL_DIGEST=88c752a3e12597b3…` **逐字未变**(8 表 29 列未破) |
| **D4** 写 home 静默空操作 | `HOME_WRITE_POLICY.onlyPath='userfs'` + 内核**零 fs 用法**(门禁扫代码行)+ 直写支**检出** |
| **E1** 测试跑的是构建产物 | 改完**必** `node scripts/build.mjs --check-only`(本棒改 `build.mjs` 自身后复验两次) |
| **E6** 禁 `exports.default` | `lib/client.js` 仍只交 `apply` + `inject`(门禁 ①b 绿) |
| **G5** ⛔ 不下发平台共享密钥 | 明文**只在写入那一条出向**上;读路径无密钥字段(S7-4) |
| **G8** 改 / 重启要写进回执 | 见 §八:⛔ 未重启任何控制面、⛔ 未对真机投放 —— 如实标注"机械判据绿 / 真机未验" |
---
## 八、尺子回归(⛔ 未改尺子一行)/ 未取到(如实)
```
⓪ 包内门禁 node scripts/build.mjs --check-only | rc=0 (85 ✅ / 0 ❌,S6 时 60 ✅)
M5_DIGEST=55cdc0d5fc8eb99d ← 与第 43/44 棒逐字相同 ⇒ M5 的绿没被弄红
M3_DIGEST=7045a778baa82cfe ← 与第 44 棒逐字相同
DECL_DIGEST=88c752a3e12597b3…(8 表 29 列未破)
M2_DIGEST=11d7b1be8aea3996 ← 本棒新增
S7-1 装配 · 端A 客户端形态(桌面 profile 真件副本 + 本包) | rc=0
S7-1 装配 · 端B 平台形态(profiles/web 键位建模 link: 依赖) | rc=0
S7-1 取数 · 真实 import 本产物 | rc=0
S7-1 回调 · 真实调用 apply(ctx)(spy ctx) | rc=0
运行 · 真启动日志的失败面(⚠️ 弱证据) | rc=0
v-neg.sh all | rc=0 (装配/运行/取数/回调 四项红绿双态均成立)
S7 取证编排 node tmp/p45/run-s7.mjs | rc=0 (73 ✅ / 0 ❌)
门禁负控 node tmp/p45/neg-gate.mjs | rc=0 (8/8:7 处改坏**全部**被 ①g 抓到)
v-baseline.sh(开工对照,尺子自述⛔ 不进判据) | rc=0 (平台侧 4 个包 / 客户端 5 个 bundle;`@dsh-local/*` 计 0)
```
### ⚠️ 未取到 / 不成立(如实,⛔ 不折算成"已知/通过")
| 项 | 读数 | 原因 |
|---|---|---|
| **浏览器级(第四张卡片的真壳)** | **`v-ui.sh` rc=3**(`UNAVAILABLE / no page target`) | 需**真壳在跑**;启动器属**客户端线**(`dsh-ai1net-desktop/.workbuddy/_devkit/launch-desktop-dev-017.mts`),本线 ⛔ 不代启 ⇒ 机械判据已绿、**视觉效果未取到** |
| M5 卡片 + S4-6 实例页能力栏 | 同上 | 同因(第 42/43 棒已欠,本棒未消) |
| `v-plugin.sh 运行` 面 | rc=0 但**空真** | 可用日志 mtime 早于本包 ⇒ **弱证据**,⛔ 不当"运行期已验" |
| 平台侧真跑(真库 / 真 `requireAdmin`) | 未取到 | 属**投放线**,本棒⛔ 不越界 |
---
## 九、本棒的 5 处「仪器错」(如实记录 · ⛔ 其中 3 处不修就是假绿/假红)
| # | 错在哪 | 症状 | 修法 |
|---|---|---|---|
| ① | 门禁 ①g 的负控**夹具**用裸字符串当每插件令牌 | 三条授权断言全报"平台零调用" —— 看着像**产品没打到平台**,其实是**门禁自己的夹具不合格**(`resolveEnableProof` 会验签) | 用 `signPluginToken()` 真签一枚(判据:夹具也要合规格) |
| ② | 门禁 ①g 第 ⑩ 条的 fs 扫描认"文件里出现过 `fs` 这个词" | 把 `HOME_WRITE_POLICY.forbidden` 那一行**自己的字面量**当成引用 ⇒ **门禁自己假红** | 改成认**用法**(`from 'node:fs'` / `fs.xxx(` / `writeFileSync(`),⛔ 不认字符串里的名字 |
| ③ | `redactModelConfig()` 首版**先判白名单、再判敏感字段名** | 敏感字段被白名单**静默丢掉** ⇒ `redactions` 恒 0:输出确实干净,但**没有证据**("看着干净"不是"证明干净") | 调换顺序:先判敏感字段名、再判白名单;门禁加一条"喂明文必须计到净化数" |
| ④ | `run-s7.mjs` 首版令牌 `exp` 用固定时刻 `1774000000000`(= 2026-03-20) | host 半内部用**默认时钟**(无注入点)⇒ 令牌**已过期** ⇒ 全部正常路径被闸拒成 `plugin-not-enabled`,73 项里 **24 项假红**。识破它的是那一行 `出向请求总数 = 0` | 改用**真时刻** `Date.now()`;并把"出向总数"打进读数摘要(假红时一眼可见) |
| ⑤ | `run-s7.mjs` 三条断言写过头 | 分别错在:身份头里其实是**调用方出示的 pluginId**(不是包名)/`effective` 本就在白名单内、**应当保留**/明文写入被调了两次 | 逐条改成与实现口径一致(⛔ 不为了"变绿"放松,而是把**正确的口径**写清) |
**产品侧真 bug:本棒 0 处**(与第 44 棒不同 —— 那棒抓到 `PKG_RE` 不收带 scope 的包名)。如实记录,⛔ 不凑数。
---
## 十、硬边界自查 / 遗留 / 回滚
- ⛔ **未 commit / 未 push**(未明确要求)。
- ⛔ **未触碰官方 dsh 主程序与缓存**(R2);⛔ 未把包装进任何 profile(装配用 `tmp/p45/` **profile 副本**)。
- ⛔ **未重造分发与更新机制**;⛔ **未代用户起实例**;⛔ **未动客户端工作区**(`dsh-ai1net-desktop` / `dsh-desktop-0.1.7rc2` 只读引用)。
- ⛔ **未重启控制面 / 未中断任何在线用户 / 未批量改文件**(红线门禁未触发);包内**零直连平台库路径**(门禁扫代码行)。
- **S9 的 P2 未跨过**;**本棒发现一处新停点**(见下一行)⇒ 依纪律**未登记下一棒**,入口写「⏸ 等拍板」并列出候选。
- 🔴 **新停点(本棒发现)= S8 的硬门未满足**:方案 §3.10.10 原文「⛔ **本步的硬门**:**F1.5(G1 序号 + G6 撤根)必须先有结论**」,而现状 **G1 未修、G6 无任何机制**(依据 `交付物/AI1NET-三类节点结构调整可行性-20260926.md` §P0「G1 未修|G6 无机制 ⇒ 规模越大越撤不回去」与 `交付物/全功能承载判定-底层插件能否承-20260926.md` §七「F1.5 = 一切前置」)。⇒ 依用户 2026-09-17 明令「要用户拍板的,等拍了再新建接续会话」,**本棒 ⛔ 未登记下一棒**(候选 A/B 与倾向见入口 §2「⏳ 等拍板」块)。
- **并发**:本棒按惯例用 guard 锚点算法自定域键(8 个);`preflight-lock.sh` 仍把**工作区外仓库目录 + 工作区根文件**恒判【D】未归类(7 个)—— 与第 26–44 棒同一处**已知误报**,⛔ 未改那个跨库脚本,如实记此条。
- **回滚**:删 `lib/models-sdk.mjs` + 撤 `lib/index.js` 的 M2 装配块与常量 + 撤 `lib/client.js` 的 M2 字典/卡片/样式段与槽位注册 + 撤 `scripts/build.mjs` 的 ①g + 撤 `package.json` 的 `exports["./models-sdk"]` 一条。⚠️ 回滚后 `M5_DIGEST` / `M3_DIGEST` / `DECL_DIGEST` 因**本就未变**,仍可作前后一致锚点。
---
## 十一、出处
- 规格书:`D:/github/dsh_shenxian/dsh-server-docs/05-交接单/交接单_包规格书_20260926.md`(§〇 · §五 A1/A2/A3/A4 · §五 C)
- 方案:`交付物/功能打包到基础插件-分步实施方案-20260926.md`(§3.10.9 = S7 逐步照做唯一权威;§4-B/D/E/G 类)
- 平台真实实现(只读引用):`D:/github/dsh_shenxian/src/web/routes/auth.ts`(484 / 526 / 598 / 617 / 664)· `src/web/routes/admin.ts`(107-133)· `src/db/repo.ts`(346 `CREDENTIAL_COLS`)· `src/db/schema.ts`(454-466 `shared_model_granted` v11)· `src/fs/user-fs.ts`(69 `UserFs` 接口)
- 上两棒口径继承:`交付物/技能插件管理-S6落地与四轮回执-20260926.md`(§六 §九)· `交付物/IM插件侧-S5落地与四轮回执-20260926.md`(§六)
- 本棒读数(⛔ 不入库):`tmp/p45/读数-S7-20260926.txt`(73 ✅ / 0 ❌)· `tmp/p45/尺读数-S7-20260926.txt` · `tmp/p45/门禁负控-20260926.txt`(8/8)
- 编排与负控脚本:`tmp/p45/run-s7.mjs` · `tmp/p45/neg-gate.mjs` · `tmp/p45/run-scales.sh`