Files
workbuddy_skills/dsh-workflow/references/dsh-change-workflow/03-沙箱与技能机制.md
T

80 lines
8.3 KiB
Markdown
Raw Normal View History

# 机制速查(一)· Profile 层 cordis patch / Skill 装载机制 / Skill 管理面
> **归属**:技能 `dsh-change-workflow` 的详情档(**按需读**,不是每次都要读)。
> **本档覆盖**:Profile 层 cordis patch 机制 · Skill 装载机制 · Skill 管理面 · dsh 沙箱与权限预设机制(原行 L481–L504 + L555–L600)。
> **主文件 / 判据与流程主干** = `../SKILL.md`(§1 六阶段流程 · §2 红线 R1–R8 原文速查 · §3 本机 Git Bash 环境坑 · §4 并行调度结论)。
> **来源**:2026-09-22「技能重组线」把 `../SKILL.md` 的 L481–L504 + L555–L600 段**逐行原样**下沉到本文件,未改一字。
> **跨档引用**:正文里的「§N / 见 §8 坑 N / 见下表」等编号,用 `../SKILL.md` 末节「详情索引」的原章节列定位。
> **维护**:本文件与 `../SKILL.md` 的指针行成对;改内容时同时核对主文件的指针描述是否仍准确。
---
### Profile 层 cordis patch 机制(角色化 UI 裁剪,档案 09)
- client 插件行 id = **短 id**(`ui-settings-models`,非包名),见 `dsh --profile web --dump-config`。
- **禁用官方 client 插件**(如设置面板「模型」分区对普通用户):profile `cordis.patch.yml` 写 `- id: <短id>\n name: "@deepseek-ai/<包名>"\n disabled: true`(dsh-app-boot applyEntryPatches:非 insert patch 按 id 合入 overrides)。`--dump-config` 验证:目标行出现 `disabled: true` + `# == ... patched by <path>` 注释。
- **生效必须重启实例**(client bundle 启动时打包);`patchReload: live` 对 client 插件增减**不生效**(实测 5 轮)。
- 当前生产 spawn 不带 `--patch` overlay(enablePatch=false),disable 只能写 profile 层 `cordis.patch.yml`。
- 幂等工具:`/opt/dshs/ensure-role-profile-patch.cjs [--restart] <username>`(全量=非 admin 用户;含管理标记头则跳过;非默认空内容不覆盖)。admin profile 不动 = admin 保留该分区。
### Skill 装载机制(全员共享只读技能 = bundledSkillDir,档案 10)
- 技能由 agent preset 注册:`@deepseek-ai/dsh-agent-presets/presets/standard/agent.cordis.yml` L83-88(`skill-filesystem` + `tool-skill`),preset 无 config 块 → provider 配置走 **env**。
- 技能分层 rank(`dsh-skill-filesystem/lib/index.js` roots()):project-dsh 100(`<项目>/.dsh/skills`)/ project-agents 200 / custom 300 / user-dsh 400(`$DSH_HOME/skills`,每用户独立)/ user-agents 500 / **bundled 600(`$DSH_BUNDLED_SKILL_DIR`,`trustedHost: true` 只读共享)**。
- 扫描粒度:`discoverRoot` **只扫 1 层**(root 下每目录须含 `SKILL.md`;或直接 .md 文件);`root/主技能/subskills/子技能/SKILL.md` **不会被发现** → subskill 要独立可调需**平铺**到技能根。
- **编排器 env 白名单(关键卡点)**:`/opt/dshs/lib/supervisor/spawn.js` `ALLOWED_ENV` 仅 PATH/HOME/USER/TMP/LANG 等基础变量;`baseEnv()` = `{...scrubEnv(process.env), HOME, DSH_HOME, DEEPSEEK_API_KEY}` → **新 env 变量必须同时加白名单 + baseEnv 显式注入**(src/*.ts 同步),否则被 scrub 丢弃。这是改编排器(自研,非红线 2 对象)。
- MCN V1.0 技能跨平台部署注意:`subskills/browser-harness/envs/` 为 **Windows venv**(`Scripts/*.exe`),Linux 服务器不可用需重建;海外节点直连抖音风控风险高,数据类优先 RedFox API。
### Skill 管理面(编排器 API + 静态页,档案 11)
- **dsh 技能名硬规则**:`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`(`@deepseek-ai/dsh-skill/lib/index.js` SKILL_NAME)—— **中文名技能 dsh 静默丢弃**。MCN 等含中文名技能需先改 `SKILL.md` frontmatter `name` 为 kebab(如 `mcn-workstation`)。
- **API 形态**:`/api/skills/shared`(admin 三件套 GET/POST/DELETE)+ `/api/skills/mine`(任何登录用户);上传 base64 body `{ file, filename, force? }`;后端走系统 `tar/unzip` 解压、路径穿越校验、合法名校验。包结构:单顶层目录 + 含 SKILL.md。
- **落盘属主**:shared → root:root 0755(OS 权限兜底只读);mine → `home` 属主用户 uid/gid(确保用户可改自己的技能)。踩坑:`chownTree` 须 chown 顶层目录自身(首次实现只 chown 子项,目录保留 tar 包内 owner)。
- **watch 即时生效**:`skill-filesystem` 对共享根 watchManager 监听 addDir/unlinkDir/SKILL.md → 自动 invalidate registry(**无需重启实例**,源码已核实)。
- **编排器 env 注入 DSH_BUNDLED_SKILL_DIR**:spawn.ts ALLOWED_ENV 加变量 + orchestrator.ts baseEnv 显式注入(`config.bundledSkillDir`,默认 `<dataRoot>/bundled-skills`);这是上一节「全员共享只读技能层」的实例侧落地。
## dsh 沙箱与权限预设机制(2026-09-11 源码核实,改默认值必看)
**两层隔离,别混淆**:
| 层 | 谁提供 | 说明 |
|---|---|---|
| **内层** | dsh 自带沙箱 `read-only` / `workspace-write` / `danger-full-access` | 靠 Landlock 或 bubblewrap 后端。**本机不可用**:内核 5.10(Landlock 需 5.13+,LSM 无 landlock),bwrap 嵌套探测失败 → **fail-closed 拒绝执行任何 shell**(`@deepseek-ai/dsh-sandbox/lib/index.js:185`)|
| **外层** | 平台 `systemd-run --scope` + `bwrap` + `setpriv` | **真正的边界**:mount 路径隔离 + uid 隔离 + cgroup(**基础 448M → 上界 1024M**:`MemoryHigh=MIN` 软限 + `MemoryMax=MAX` 硬限;**与插件开关无关**(档案 96)/150%/128)。内层失效不影响它 |
**权限预设插件**(row `@deepseek-ai/dsh-permission-presets`,短 id 一般即 `permission-presets`):
- 内置预设表(**sandbox 与 approval 成对绑定**,这是关键坑):
```js
'workspace-write': { sandbox: 'workspace-write', approval: 'ask' }
'danger-full-access': { sandbox: 'danger-full-access', approval: 'never' } // ← 选它=同时摘掉审批
```
- `defaultPreset = config.defaultPreset ?? inferredDefault`;默认落 `workspace-write`(→ 本机即「shell 全废」)。
- 设置持久化命名空间 = **`permission`**(settings key `permission.defaultPreset`,值必须是 `presets` 里的键名)。
**改默认值的官方做法(不碰官方主程序,R2 合规)**:
> ⭐ **首选:`DSH_PERMISSION_MODE` 环境变量**(2026-09-11 定案,档案 33)。
> 官方 `@deepseek-ai/dsh-base/cordis.patch.yml:217/233` 直接读它:
> ```yaml
> mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
> policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'workspace-write') === 'danger-full-access' ? 'never' : 'ask'"
> ```
> 在编排器 spawn 时注入 `DSH_PERMISSION_MODE=danger-full-access` 即可 **全局默认「完全权限」+ 免审批**,且:
> ① 不必铺 profile(env 随 spawn,**新用户自动生效**);② 完全复用官方逻辑(预设表 / 设置 UI 都不动)。
> 落地两处:`spawn.ts` 的 `ALLOWED_ENV` 加该键 + `orchestrator.ts` 的 `baseEnv()` 注入(值取 `process.env.X ?? 'danger-full-access'`,运维可覆盖)。
> ⚠️ **副作用**:权限档位是**会话级播种**(会话创建时写 `permission/preset` / `sandbox/mode` / `approval/policy` 三条种子事件)→ **旧会话不跟随新默认**,必须**新建会话**才生效。给用户的话术是「新开一个会话」,不是「刷新页面」。
其余做法(需要更细粒度时才用):
1. **profile 层 `cordis.patch.yml` 改该行 `config`** —— 已核实 `applyEntryPatches` 里
`const { id, insert, name, ...overrides } = patch` → **patch 的其它键会作为 overrides 合入目标行**,所以 `config:` 可覆盖。
注意 **`config` 是整体替换不是深合并** → 必须把内置的两个预设一并写全。
2. 或改设置:`$DSH_HOME/settings.yaml` 加
```yaml
permission:
defaultPreset: <presets 里的键名>
```
3. **profile 是每用户一份** → 要像 `ensure-role-profile-patch.cjs` 那样给每个用户(含新用户)铺;改完**必须重启该用户实例**才生效。
4. 用户仍可在实例「设置 → 权限」自助切换(`ui-permission`),这是设计内的。
**推荐形态**:不要直接用 `danger-full-access`(会连审批一起摘掉)。在 patch 里**新增一个自定义预设**:
`sandbox: danger-full-access` + `approval: ask`,语义 = 「隔离由平台容器提供,高风险操作仍需确认」。