mirage
strukto-ai
The World's First Unified Virtual Filesystem For AI Agents
PROJECT TOPICS
INSTALL REFERENCE
dsh plugin --profile web add github:xgone/dsh-remote
该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。
PROJECT README
English | 中文
让 DeepSeek Harness 可以被安全地远程访问:在 dsh web 前增加完整的账号密码认证 + MFA
(两步验证)门禁,并打通外网部署所需的全部环节——外部浏览器登录后即可使用完整功能(含工作区
选择、添加工作区等),全程不会在宿主机上弹出任何原生窗口。
| 登录门禁(未登录访问任何路径) | 设置 → 登录与账号 |
|---|---|
![]() |
![]() |
/api 信任围栏把 host.pickDirectory、settings.*、
credentials.* 等特权方法硬编码钉死在 loopback(官方注明"直到真正的认证层出现")——本插件
就是那个认证层,认证通过后自动放行。events.mux / events.host)经认证后正常建立。/api 与 WebSocket 全部要求有效
会话 Cookie。N=16384,r=8,p=1)+ 常量时间比较,绝不落盘明文。adminOnly 后可启用 admin / user / guest 三层方法级权限。zh / en)插件的全部界面——登录页、MFA 绑定引导、重登浮层、设置页(登录与账号)——均提供中英双语,跟随 DSH 应用语言设置(Settings → General → Language):
$DSH_HOME/settings.yaml 的 locale.preference(即 DSH 应用语言),未设置时回落浏览器 Accept-Language;@deepseek-ai/dsh-client-locale 服务(ctx.locale),注册插件的 zh/en 字典并实时跟随语言切换——切换语言时设置页与浮层即时更新,无需刷新。远程浏览器语言持久化:DSH 出于安全设计把整个设置面钉死在 loopback(本机)——远程浏览器的设置读写只进内存,语言偏好在 DSH 原生机制下刷新即丢。本插件作为远程部署的认证层,为非 loopback 浏览器接管了语言配置的双向通道:
settings.describe RPC 取回持久化偏好并应用到界面(登录页也在服务端读同一份偏好);settings.mutate RPC 落盘 settings.yaml,跨浏览器、跨设备一致。本机(127.0.0.1 / localhost)访问完全走 DSH 原生 host-backed 路径,插件不做任何介入。两条通道都基于 DSH UI 自身使用的标准 RPC 信封,不魔改任何宿主内部状态。
dsh web(默认端口 3080);web profile(首次运行 dsh web 会自动初始化);pnpm(dsh plugin 命令需要它来管理 profile 的插件)。NPM 安装(推荐)——插件已发布到 npm registry,一行命令即可:
dsh plugin --profile web add @xgone/dsh-remote
- 包主页:https://www.npmjs.com/package/@xgone/dsh-remote
- 固定版本:
dsh plugin --profile web add @xgone/dsh-remote@0.1.0
其他安装方式:
# 从公开 Git 仓库安装
dsh plugin --profile web add git@github.com:xgone/dsh-remote.git
# 本地源码目录(开发/自用,路径替换为你实际的源码位置)
dsh plugin --profile web add ~/path/to/dsh-remote
dsh plugin 会做三件事:
~/.dsh/profiles/web/ 下用 pnpm 安装该包(输出 + @xgone/dsh-remote 即成功);@xgone/dsh-remote 追加到 profile 的 dsh.profile.bundles(因为包声明了
dsh.bundle.patch)——无需手动改配置;# bundles 列表中应出现 @xgone/dsh-remote
python3 -c "import json; print(json.load(open('$HOME/.dsh/profiles/web/package.json'))['dsh']['profile']['bundles'])"
# 期望输出类似:['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app', '@xgone/dsh-remote']
dsh webWeb 表面目前禁用 HMR,补丁热重载不可用,必须重启才生效:
# 结束当前 dsh web 进程后重新启动(或直接重启你的启动方式)
dsh web
重启后,浏览器打开 http://127.0.0.1:3080:
验证:
curl http://127.0.0.1:3080/auth/me应返回{"authEnabled":true,"bootstrap":false,"authenticated":true,...}。
登录后进入 设置 → 登录与账号 → 双重验证 (MFA) → 启用双重验证:
也可以不进入设置页:登录页在密码验证通过后(未开启 MFA 时)会直接给出"绑定双重验证"引导。
/api → 403;/api、WebSocket 事件流、工作区选择(浏览器内目录对话框)均正常;dsh plugin --profile web remove @xgone/dsh-remote
dsh plugin 会执行 pnpm remove,并自动把该包从 dsh.profile.bundles 移除;
重启 dsh web 后门禁即消失。已创建的 $DSH_HOME/auth/store.json 与账号数据会保留
(如需彻底清除可手动删除该文件)。
| 现象 | 处理 |
|---|---|
| 安装后无登录页 | 未重启:执行 dsh web 重启;或 bundles 列表里没有 @xgone/dsh-remote(重跑 dsh plugin --profile web add ...) |
| 登录页提交无反应 | 确认表单已填用户名/密码;浏览器控制台的 content-script.js 报错是扩展噪音,可忽略 |
| 创建管理员时报 403 | bootstrap 仅限 loopback:请在本机浏览器操作,或经 ssh -L 后访问 127.0.0.1;无本地浏览器的服务器请用下方 bootstrap 配置预置管理员 |
| 被锁在门外(配置出错) | 编辑 cordis.patch.yml 设 enabled: false 重启;或删除 $DSH_HOME/auth/store.json 重新引导 |
| 忘记 MFA / 丢手机 | 管理员登录后在 设置 → 登录与账号 → 该账号行 → 禁用 MFA(需管理员密码) |
| 远程访问时「设置 → 插件」配置页空白 | v0.1.5+ 已内置修复:DSH 对远程浏览器把所有设置 scope 切成 memory 模式(读写在客户端被丢弃),插件启动时自动解除该限制并触发一次全量刷新,配置卡片远程可读可写;原始 settings.yaml 文档编辑器仍保持仅限本机(设计如此) |
| 远程刷新后反复弹出「内测声明」 | v0.1.6+ 已内置修复(已确认用户):DSH 的欢迎弹窗确认态 WelcomeNoticeStore 对远程走 memory 模式,不读已持久化的确认;插件用官方 settings.describe RPC 读 ui-onboarding.welcomeNoticeVersion,有值时经官方 store.update 置 acknowledged=true,WelcomeNotice 即自动收场不再弹。全程只用官方 slots/store/RPC,不重写 DSH 内部方法、不改 persistence。纯远程首次用户(从无确认)维持原行为 |
服务器没有本地浏览器时,UI 的「创建管理员」引导(仅限 loopback)无法使用。用 bootstrap
配置节在启动时直接预置首个管理员(等价于 loopback 引导:同样受保护、管理员角色、scrypt 哈希):
dsh plugin --profile web add @xgone/dsh-remote
# 编辑 ~/.dsh/profiles/web/cordis.patch.yml,给 remote 行加 config:
- id: remote
config:
enabled: true
bootstrap:
username: admin # 首次启动时创建(仅当账号库为空)
password: '换成一个强密码'
要点:
protected: true(不可删除/降级,仅可重置密码);cordis.patch.yml 移除该配置节(保留也无风险——账号存在后即失效),
之后可在 设置 → 登录与账号 绑定 MFA;trustProxy: true(默认已开启)。cordis.patch.yml)# ~/.dsh/profiles/web/cordis.patch.yml(覆盖 remote 行的整份 config,缺省键回落到默认值)
- id: remote
config:
enabled: true
accounts: [] # 种子账号(明文密码启动时转 scrypt 哈希;或直接给 scrypt$<salt>$<hash>)
secret: '' # 空 = 自动生成并持久化
session:
cookieName: dsh_session
ttlSeconds: 604800 # 7 天
secure: false # HTTPS 部署建议 true
sameSite: lax
enforceRoles: true # admin/user/guest 方法级权限
adminOnly: true # 仅管理员账号(默认)
trustProxy: true # 已认证请求归一化 Host/Origin,放行外部访问(默认)
bootstrap:
username: admin # 可选:无浏览器服务器预置首个管理员(账号库为空时生效)
password: '...'
mfa:
enabled: true
issuer: DeepSeek Harness
window: 1 # ±1 个 30 秒步长
backupCodes: 10
rateLimit:
maxAttempts: 5
windowMs: 900000
关闭认证:enabled: false。
插件是 Cordis 双半区包 + bundle 补丁:
dsh-remote/
├── cordis.patch.yml # bundle 补丁:插入 remote 行;替换目录选择器后端
├── lib/
│ ├── index.js # 宿主半区:门禁、会话、限速、角色、trustProxy、/auth/* 路由
│ ├── store.js # 账号存储(scrypt 哈希、受保护标记、TOTP 状态、备用码哈希)
│ ├── totp.js # RFC 6238 TOTP / base32 / otpauth URI / 备用码(零依赖)
│ ├── login-page.js # 自包含登录页(密码 → 动态码 → 绑定引导,含二维码)
│ └── client.js # 浏览器半区:重登浮层、设置页「登录与账号」、退出按钮
└── package.json # dsh.bundle.patch + dsh.client 声明 + ./client 导出
dsh plugin 自动把 @xgone/dsh-remote 追加进 dsh.profile.bundles,补丁随 composition 应用;inject: ["webServer"] 激活,浏览器半区由客户端模块系统扫描进
window.__DSH_BOOT__ 并挂到 settings.section / 覆盖层。lib/index.js)DSH 的 webServer 路由模型是"精确表 → 前缀表 → fallback",没有中间件钩子。插件通过
包装路由注册方法 + 就地包装已注册表项实现全量门禁:
register / registerUpgrade / registerFallback 三个方法,并对
webServer.exact / prefixes / upgrades 表与 fallback 中已注册的每一项逐个包装(用
Symbol 标记避免重复包装,WeakMap 记录原始处理器以便卸载还原);/auth/* 放行 → 无有效会话则页面请求返回登录页、其余 403 →
通过后先归一化 Host/Origin 再交给下层(见 trustProxy);method 查禁止表,命中 403,
放行时用可重放请求体(Readable + Proxy)交给下层,admin 零开销。v1.<base64url payload>.<HMAC-SHA256 sig>,payload 含
{sub, role, iat, exp};验证时重算签名并常量时间比较,且账号必须仍存在(删除即失效);mfa.<payload>.<sig>,5 分钟有效、一次性(nonce 消费集合防重放);trustProxy)DSH 内置的浏览器信任围栏(dsh-client-connection)校验 Host(loopback 或 trustedHosts)、
Origin 与 Host 一致、sec-fetch-site 非 cross-site,且特权方法(host.pickDirectory、
settings.*、credentials.* 等)被硬编码钉在 loopback。插件在认证通过后把请求的
Host/Origin 归一化为 127.0.0.1:<端口>(保留原 scheme)再交给下层——围栏判定为 loopback,
特权方法对外部浏览器放行。未认证请求仍被门禁 403,围栏的 DNS-rebinding 语义在 Cookie 之后不再
需要。
lib/store.js)$DSH_HOME/auth/store.json(0600,原子写入):
{
"version": 1,
"secret": "<base64url 32B>", // 会话/MFA 令牌签名密钥
"accounts": [{
"username": "admin",
"role": "admin",
"passwordHash": "scrypt$<salt>$<hash>",
"protected": true, // 首个账号:不可删/不可改角色
"totp": { "secret": "<base32>", "verified": true, "createdAt": 0 }, // 未启用则为空
"backupCodes": [{ "hash": "<sha256>", "usedAt": 0 }], // 备用码仅存哈希
"createdAt": 0, "updatedAt": 0, "lastLoginAt": 0
}]
}
迁移逻辑:老存储无 protected 字段时,按 createdAt 最早的账号自动标记为受保护并落盘。
lib/totp.js,零依赖)otpauth:// URI 构造;lib/login-page.js)由被包装的 fallback 在未认证时直接内联输出,零外部资源。阶段机:
password → (code | offer) → setup:
/auth/mfa/setup 返回的 SVG data URL)+ 密钥 + otpauth + 备用码 +
验证输入,验证成功跳转 next;novalidate + JS 手动校验(避免隐藏必填控件阻塞提交)。lib/client.js)手写 window.__ModuleLoader__.load({id, factory}) 格式(与官方包一致,无需打包),通过
require("react") / react-dom/client 挂载:
/auth/me 轮询(15s + focus),未认证且启用认证时全屏覆盖登录(支持 MFA 第二步);settings.section slot,带用户图标,用 CSS 隐藏 shell 默认齿轮):
状态、MFA 自服务、账号卡片列表(重置密码/禁用 MFA/删除)、退出登录;fetch("/auth/*")(同源 Cookie),不依赖 settings 域(第三方程无法注册)。cordis.patch.yml)补丁行不能改名(name mismatch 会被跳过),因此用"禁用 + 插入":
- id: directory-picker # 禁用 auto 选择器(loopback 下会解析成 native、在宿主弹窗)
disabled: true
- insert:
- id: directory-picker-browse # browse 宿主后端(host.listDirectory / createDirectory)
name: '@deepseek-ai/dsh-host-directory-picker-browse'
config: { maxEntries: 1000 }
- id: directory-picker-browse-ui # 浏览器端目录对话框
name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'
| 攻击面 | 防护 |
|---|---|
未认证访问 /api / 静态页 / WebSocket |
门禁 403 / 登录页 / 握手拒绝 |
| 密码爆破 | scrypt + 限速(IP+用户名) |
| 会话伪造/篡改 | HMAC 签名 + 过期 + 常量时间比较 + 账号存在性校验 |
| 会话重放(MFA 第二步) | 一次性 nonce + 5 分钟 TTL |
| 远程抢先注册管理员 | bootstrap 仅 loopback |
| 误删唯一管理员 | 受保护账号 + 最后管理员保护 |
| 跨站请求(CSRF) | HttpOnly + SameSite=lax;跨站请求带不上 Cookie,门禁即 403 |
| DNS rebinding | 认证层已接管围栏语义;未认证请求不会到达围栏 |
dsh web 仍拒绝 --host 0.0.0.0,请用反向代理暴露(TLS 终结 + WebSocket 转发):
server {
listen 8443 ssl;
server_name dsh.example.com;
ssl_certificate /etc/letsencrypt/live/dsh.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dsh.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # WebSocket(events.mux/host)
proxy_set_header Connection "upgrade";
proxy_set_header Host $host; # 保留外部 Host
proxy_set_header Origin $http_origin;
}
}
同样适用于 ssh -R 隧道、Tailscale、Frp 等。HTTPS 部署时把 session.secure 设为 true。
$DSH_HOME 进程级
共享),事件流、搜索、任务等全局泄漏无法在插件层隔离;dsh --profile alice --port 3081)天然隔离数据;共享实例
协作则用角色体系。| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /auth/login |
登录;有 MFA 时返回 {mfaRequired, mfaToken}(不签 Cookie) |
| POST | /auth/mfa/login |
第二步:{mfaToken, code}(动态码或备用码) |
| POST | /auth/logout |
清除 Cookie |
| GET | /auth/me |
{authEnabled, bootstrap, authenticated, username, role, mfa, adminOnly} |
| POST | /auth/bootstrap |
首个管理员引导(仅 loopback、仅空存储) |
| POST | /auth/mfa/setup |
生成密钥 + otpauth + 二维码 + 备用码(pending 状态) |
| POST | /auth/mfa/verify |
用动态码确认 pending 设置 |
| POST | /auth/mfa/disable |
关闭本账号 MFA(需密码 + 有效动态码/备用码) |
| POST | /auth/accounts |
管理员:{action: list\|upsert\|remove\|disable-mfa, ...} |
dsh web;enabled: false 重启即恢复;或删除 $DSH_HOME/auth/store.json 重新引导;CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: security。