dsh-tenancy
为 DeepSeek Harness(DSH)打造的单实例多租户插件:借助 Caddy + Authelia 认证前置提取用户身份,通过会话级 owner/access ACL、影子路由门控与事件流过滤实现会话/工作区的可见性隔离,并以工作区围栏与共享、邀请码注册、公开站点发布等能力,让互信团队安全共用一个 dsh 实例。
- Stars
- 0
- Language
- JavaScript
- Created
- Aug 27, 2026
- Updated
- Sep 8, 2026
Introduction
@choi-p/dsh-tenancy
[!NOTE] 本文档由 AI 生成,可能存在错误或遗漏,使用前请 review 并实测。
DSH 单实例多租户插件 —— 配套 Caddy + Authelia 认证前端使用。
所有用户共享全局模型 API_KEY 与插件运行环境,适用于成员间信任度较高的内部团队、
小型工作组或亲友共享场景,不适用于需要严格资源隔离的环境。
本 README 同时是整套认证前端的部署手册。
浏览器 ── TLS ──▶ nginx :443
│ dsh.example.com ──▶ Caddy 127.0.0.1:9443(HTTP)
│ /auth/* ─────────▶ Authelia 127.0.0.1:9091(门户)
▼
Caddy:forward_auth 问 Authelia → 注入 Remote-User/Groups
普通路径: Host 固定为公网域名
特权方法路径(/api/settings.* 等15个): Host→localhost + 剥 Origin
▼
dsh 127.0.0.1:3088 + 本插件(exact 影子路由做会话级 owner/access ACL)
快速部署
sudo bash install.sh --domain dsh.example.com
# 可选: --admin-user <name> --admin-password <pass> --github-proxy https://ghproxy.net/
脚本自动完成全部步骤:下载二进制、生成密钥、创建管理员账号并保存凭据至
/root/dsh-p0-credentials/admin.txt。
前置条件
| 项 | 要求 |
|---|---|
| 已有 | nginx 占 443 且有可用证书的站点 |
| 端口空闲 | 127.0.0.1:9091(Authelia)、127.0.0.1:9443(Caddy),均只绑回环 |
| DNS/证书 | 不需要新增,复用现有域名与证书 |
版本参考:Authelia v4.39.20、Caddy v2.11.4。
手动部署要点
install.sh 会自动完成以下全部步骤。手动部署时参照 examples/ 目录:
- 安装二进制 — Authelia →
/opt/authelia/,Caddy →/usr/local/bin/, 创建各自服务账号 - 部署 Authelia — 复制
configuration.yml和users.yml到/etc/authelia/, 填入随机密钥和口令哈希 - 部署 Caddy — 复制
Caddyfile到/etc/caddy/, 生成共享密钥DSH_TENANCY_SECRET - 接入 nginx — 参照
dsh.example.com.conf修改 vhost
Authelia 关键点 (v4.39)
- 访问控制规则顺序敏感:admins 放行 → 同路径显式 deny → team 放行其余。 缺少 deny 规则会导致非管理员穿透
插件功能
- 身份提取 — 读 Caddy 注入的
Remote-User/Remote-Groups; SSH 隧道直连按本地管理员处理 - 会话 ACL — 影子路由接管会话类 RPC,按
owner/access判定可见性;session.list/search/workspace.list响应自动过滤 - 事件流隔离 — WebSocket 下行逐帧过滤,未授权会话零帧泄漏
- 管理面 —
/tenancy/*提供 whoami / sessions / claim / acl / invites 接口 - Client UI — 会话头「共享」按钮 + owner 徽章 + 设置页多租户卡片(含登出)
- 工作空间围栏 — 非管理员的目录浏览、新建工作区、新建会话全部限制在
~/dsh(可配)内; 浏览越界静默钳制到根,写入越界 403 - 邀请码注册 — 管理员生成一次性邀请码,新成员通过
/register自助注册 - 审计日志 —
$DSH_HOME/tenancy/audit.log(JSONL,5MB 轮转)记录门控拒绝、 ACL 变更等 - 公开站点(P11) — 独立端口匿名静态发布:把公开域名(如
pub.example.com) 经 nginx/Caddy 转发到127.0.0.1:3089,则pub.example.com/<user>/<projectName>直接公开个人工作区里构建好的项目 (~/dsh/<user>/<projectName>/dist,构建输出目录名可配)。无鉴权、只读, 只服务构建产物,不暴露源码/隐藏文件;详见 docs/architecture.md 与 handbooks/operations.md - 发布指引 skill — 插件启动时经
ctx.skills注册publish-web-appskill (skills/publish-web-app.md):DSH agent 编写/构建 Web 应用时自动遵循 「产物放<项目>/dist/、资源相对引用」的约定,含最简单免构建单index.html的写法;该文件也可直接拷入~/.dsh/skills/使用 - 系统用户开通(P12) — 新成员注册成功即创建同名 Linux 系统用户:
nologinshell + 无密码(shadow 固有锁定)+ 不进任何管理组 ⇒ 不可登录服务器; 其唯一文件权限是个人工作区~/dsh/<user>(dsh 以 root 运行时即/root/dsh/<user>):递归 chown + 目录 0700。幂等可重放;同名既有系统账号 不符时判 conflict 不接管;存量成员可经POST /tenancy/sysuser补建 - 工作区共享(P9 + P13 深化) — owner/admin 在侧栏把工作区「共享」给成员
(
/tenancy/workspaces/<id>/share+ UI 对话框):共享用户在侧栏可见该工作区、 可在其中新建会话(agent 真实读写同一份文件),并可在共享工作区浏览文件树/ 建目录(P13 放开,非共享路径仍钳回个人根)。共享工作区内新建的会话默认 对参与者(owner+共享用户)可读——共享前已建的会话保持私有、不追溯,owner 可随时把任意会话改回私有;写仍仅限 owner
⚠️ 多实例共享 $DSH_HOME 的红线
同时跑多个 dsh 实例时,每个启用 tenancy 的 profile 必须配置独立的 dbPath /
auditPath / invitesPath,否则后写者会清掉前者的 ACL/邀请码记录。
拿不准就只在生产 profile 启用 tenancy。
插件安装与配置
# 安装
dsh plugin --profile web add github:choi-peng/dsh-tenancy
# 应用全部补丁(dsh 升级后重跑)
bash scripts/apply-patches.sh && pm2 restart dsh-web
sharedSecret必须与/etc/caddy/dsh.env的DSH_TENANCY_SECRET一致。配好后, 未携带X-Dsh-Tenancy-Key的请求会被直接 401(不再回落为 local 管理员); 留空则任何能直连127.0.0.1:3088的进程都是 admin,不得用于生产。 核对:dsh --profile web --dump-config | less
日常运维
# 生成密码哈希
/opt/authelia/authelia crypto hash generate argon2 --password '新成员密码'
# → $argon2id$v=19$m=65536,t=3,p=4$...
# 加成员:编辑 /etc/authelia/users.yml 追加用户(password: "$argon2id$...";groups: dsh-team;管理员额外加 dsh-admins)
systemctl restart authelia
- 用户库改动后若无变化则需重启 Authelia
- 初始凭据由
install.sh生成至/root/dsh-p0-credentials/(chmod 700), 登录后尽快改密
系统用户(P12)
注册成功时自动 useradd -M -s /usr/sbin/nologin 建同名账号(无密码、不进管理组
⇒ 不可登录),并把 ~/dsh/<user> 递归 chown 给它、目录 0700。相关操作:
# 查看某成员的系统账号(home 应为 /root/dsh/<user>,shell 应为 nologin)
getent passwd alice
# 为存量成员(功能启用前注册)补建账号与目录归属(SSH 隧道直连 3088 即 local admin)
curl -s -X POST http://127.0.0.1:3088/tenancy/sysuser \
-H 'content-type: application/json' -d '{"username":"alice"}'
- 该账号唯一的文件权限是其个人工作区;后续由 root(dsh)在区内新建的文件 不会自动跟随归属,可重放上面的补建端点(幂等,全树重新 chown)
/root常为 0750,系统用户无法穿越抵达个人目录——插件会在日志提示一次; 确需真实可访问时自行决策:chmod o+x /root(仅允许穿越、不可列目录) 或setfacl -m u:<user>:x /root。插件不会擅改 /root 权限
故障排查
| 症状 | 原因与解法 |
|---|---|
forward_auth 全 400,Authelia 报 insecure scheme | Caddyfile 加 servers { trusted_proxies static private_ranges },让 nginx 的 X-Forwarded-Proto 生效 |
wrong argument count ... after '-Remote-Name' | header_up -X 一行只能删一个字段,多字段拆成多行 |
Authelia 报 missing host value | forward_auth uri 忘了子路径前缀:应为 /auth/api/authz/forward-auth |
| 非管理员也能调 settings.* | 缺少显式 deny 规则;检查规则顺序 |
新用户登录失败 does not exist | systemctl restart authelia |
成员建工作区报 path outside workspace root | 目标目录不在围栏根内,预期行为 |
/register 打不开(302 跳门户) | Caddyfile 未放行 /register 公开路由 |
| 登录页没有「邀请码注册」按钮 | nginx /auth/ location 未加 sub_filter |
注册报 server-error | 检查 dsh 进程对 autheliaUsersPath 可写、autheliaBin 存在可执行 |
| 注册成功但没有系统账号 | systemUserEnabled 未开/进程非 root/找不到 nologin shell → 看启动 WARN;存量成员经 POST /tenancy/sysuser 补建 |
审计出现 sysuser.conflict | 同名既有系统账号 home/shell 与约定不符,插件不接管不动文件;人工裁决 |
审计出现 sysuser.chown-partial | 个人目录部分条目 chown 失败(见审计 detail);重放 POST /tenancy/sysuser |
| GitHub 下载超时 | 用镜像前缀 https://ghproxy.net/ |
选模型/新建会话报 agent-presets: refusing to compose an unscoped context; the scope key is what joins an agent to its preset(resume failed for session ...) | profile 的 node_modules/@deepseek-ai 里被装进了与 dsh 内置同名的核心包副本(dsh-scope/dsh-agent-presets/dsh-session 等)——进程内同一份代码加载两遍,私有 Symbol 作用域标记跨副本不可见,AgentPresets.mount 把已作用域的 agent 上下文判成 unscoped。解法:删除 profile overlay 里与内置树重复的 @deepseek-ai/*(保留仅 overlay 独有的包,如 dsh-client-ui-primitives/dsh-client-ui-slots,以及作为第三方插件正式依赖的 dsh-settings/schemastery 等):cd ~/.dsh/profiles/<name> && pnpm remove @deepseek-ai/<dups…> && pm2 restart dsh-web。核对:ls ~/.dsh/profiles/<name>/node_modules/@deepseek-ai/ 里不应出现 dsh-scope/dsh-agent-presets/dsh-session/dsh-client-connection 等内置核心。若重装后复发,检查是否用 pnpm add @deepseek-ai/*@<版本> 手工补齐过内置依赖 |
清理重复后跑 pnpm install/peers check 报 Issues with peer dependencies found(第三方插件把 @deepseek-ai/* 声明为 peer,运行时由外层 base 树 ~/.dsh/profiles/node_modules 提供,pnpm 看不到) | 让 pnpm 不再自动补装/告警这些 peer(否则 auto-install-peers 会把核心包又装回 overlay,复发改 bug):profile 的 pnpm-workspace.yaml 顶层加 autoInstallPeers: false 与 peerDependencyRules.ignoreMissing: ['@deepseek-ai/*'](pnpm ≥10 设置读 pnpm-workspace.yaml,不再读 package.json 的 pnpm 字段;.npmrc 里不要留 auto-install-peers=true)。改完 pnpm install && pnpm peers check 应输出 No peer dependency issues found |
示例文件
| 文件 | 说明 |
|---|---|
examples/Caddyfile | 含 @adminapi 特权路径重写 |
examples/authelia/configuration.yml | 密钥已脱敏 |
examples/authelia/users.yml | 用户库模板 |
examples/nginx/dsh.example.com.conf | nginx vhost |
examples/systemd/ | Authelia / Caddy systemd 单元 |