Skip to content

Releases: Linjiangxian0203/dsh-remote-tunnel

dsh v0.2.1

Choose a tag to compare

@github-actions github-actions released this 03 Oct 07:08

中文 | English

体验优化

  • 新增右侧栏「远程连接」面板(指南页图标为显示器):新建空会话也能打开远端 dsh web —— 补上 0.2.0 遗留的唯一缺口(命令卡片要有对话历史,状态条在 hero 布局里不渲染)
  • 面板标题旁显示当前插件版本(桌面端「插件」页对任何插件都不显示版本号)
  • 面板内管理隧道:启动 / 断开(两步确认)/ 在侧栏打开 / 在浏览器打开 / 刷新,并且跟随所选主机
  • 自动发现 ~/.ssh/known_hosts 里连接过的主机,一键「添加」写进插件配置;~/.ssh 全程只读,被哈希的条目只报数量
  • 面板里可「+ 手动添加主机」原地填表(别名/主机/端口/用户/workspace)建主机;一台主机都没有时给出三步引导(添加 → 启动隧道 → 在侧栏打开),不用命令行也能上手

其他变更

  • 面板内可增删插件配置里的主机(两步确认);~/.ssh/config 条目只读,但可以「隐藏」;发现的主机可以「忽略」,「已隐藏」区一键恢复(只写插件配置,不动 ~/.ssh)
  • 命令行 hosts add / hosts rm 与面板走同一套校验;hosts 列表也会列出发现的主机
  • 路由失败按语义返回:400(参数)/ 409(已存在)/ 404(未知主机)

Improvements

  • New right sidebar "remote connections" panel (a monitor glyph in the guide): open the remote dsh web even in a brand-new session — the last gap left by 0.2.0 (the command card needs conversation history, and the status strip is not rendered in the hero layout)
  • The pane shows the live plugin version beside its title (the desktop app's Plugins page renders no version for any plugin)
  • Manage the tunnel from the pane: start / disconnect (two-step) / open in sidebar / open in browser / refresh, following the selected host
  • Hosts from ~/.ssh/known_hosts are discovered automatically and can be added with one click; ~/.ssh stays read-only and hashed entries are reported as a count
  • + 手动添加主机 opens an inline form (alias / host / port / user / workspace), and a pane with no host at all shows a three-step guide — no CLI needed to get started

Chores

  • Add or remove the plugin's own host definitions from the pane (two-step); ~/.ssh/config entries stay read-only but can be hidden, and a discovered candidate can be ignored — the 已隐藏 section restores either (the plugin's own config only, never ~/.ssh)
  • CLI hosts add / hosts rm share the panel's validation, and hosts also lists discovered hosts
  • Route failures answer with their meaning: 400 (input) / 409 (exists) / 404 (unknown host)

dsh v0.2.0

Choose a tag to compare

@github-actions github-actions released this 02 Oct 11:07

中文 | English

中文

新增

  • 桌面端面板:远程 dsh web 可直接开在 DSH 桌面应用右侧栏的「浏览器」标签里(ctx.sidebarRight.openTab("browser", …));浏览器半是手写的 bundle(src/client.js,window.__ModuleLoader__.load 格式),不需要构建步骤 —— npm、GitHub、本地目录三种安装方式拿到的是同一个文件
  • 两种打开方式:「在侧栏打开」与「在浏览器打开」(宿主直接调用与 CLI 相同的打开器);openIn(ask | browser | panel)决定默认值
  • 命令卡片:每条 /remote … 命令都会渲染一张卡片,包含命令输出、隧道状态与操作按钮 —— 在侧栏打开 / 在浏览器打开 / 启动隧道(up)/ 断开连接(down,两步确认)/ 刷新。客户端只对第一方命令做本地化,因此命令行与卡片都提供中英文案
  • 输入框常驻状态条(dock,默认开启):一行显示隧道状态与同样的操作,不必翻历史找卡片
  • 宿主路由 GET /remote-tunnel/{status,open,up,down}:每个请求都过 ctx.connection.admit()(与 /api 通道同一套 Host/Origin 栅栏 + 浏览器会话 cookie 校验),未通过返回 401/403 —— 因为这些路由会下发携带一次性 launch token 的 URL;确实无法携带 cookie 的载体可用 auth: false 显式关闭
  • 本地化:locale/en.json + locale/zh.json(meta.title / meta.description),插件卡片跟随界面语言(中文界面显示「远程隧道 (dsh-remote-tunnel)」)
  • row 配置(home / openIn / autoOpen / dock / auth):可在「设置 → 插件」编辑,默认值在代码里兜底

问题修复

  • 模式判定不再依赖邻近行的 webStartup 服务,改用 profileContext.name(web/desktop):修掉一个启动竞态 —— 判错会让插件走 CLI 分支,program.help() → appExit(0) 把整个桌面端 / dsh web 宿主进程退掉
  • Config schema 不能用 .default(...):在 dsh 0.2.0-rc.2 上会让合并后的 row 配置变成未解析的 schema 对象({}),配置值静默丢失;现在只写纯类型,默认值放在 readSettings() 里
  • peer 范围放宽为 ^0.1.2-alpha.3 || ^0.1.7-rc.1 || ^0.2.0-rc.2,一个包同时服务两条运行时线

其他变更

  • service 模式只创建一个 TunnelManager,由 /remote 命令与 HTTP 路由共享

已知行为(外壳规则,不是故障)

  • 纯新会话里看不到命令卡片:会话视图在没有模型历史时显示欢迎页,命令记录不属于模型历史;发出第一轮对话后,之前的卡片会一起出现(命令早已执行成功)
  • 状态条只在"会话已有内容"时渲染:它位于输入框的 dock 座位,而全新会话用的是居中(hero)布局,该座位不渲染 —— 0.2.1 会增加侧栏「远程主机」面板,任何会话状态都能点到

English

Added

  • Desktop panel: the remote dsh web opens inside the DSH desktop app's right sidebar Browser tab (ctx.sidebarRight.openTab("browser", …)). The browser half is a hand-written bundle (src/client.js in the window.__ModuleLoader__.load format) that needs no build step — npm, GitHub and local-directory installs ship the same file
  • Two ways to open: "Open in sidebar" and "Open in browser" (the host runs the same opener the CLI uses); openIn (ask | browser | panel) chooses the default
  • Command card: every /remote … command renders a card with its output, the tunnel state and the actions — open in sidebar / open in browser / start tunnel (up) / disconnect (down, two-step confirmation) / refresh. The client localizes only first-party command copy, so both the command row and the card ship bilingual text
  • Composer status strip (dock, on by default): one line of tunnel state with the same actions, so the tunnel is reachable without digging through history
  • Host routes GET /remote-tunnel/{status,open,up,down}: each request passes ctx.connection.admit() — the same Host/Origin fence and browser-session cookie the /api channel uses — and answers 401/403 otherwise, because these routes hand out a URL carrying a one-time launch token. auth: false opts out for a carrier that cannot present a cookie
  • Localization: locale/en.json + locale/zh.json (meta.title / meta.description), so the plugin card follows the UI language
  • Row configuration (home / openIn / autoOpen / dock / auth), editable on the Plugins page, defaults resolved in code

Fixed

  • Mode detection no longer relies on the neighbouring webStartup service but on profileContext.name (web/desktop), removing a startup race where a wrong guess took the CLI branch and program.help() → appExit(0) terminated the whole desktop/web host process
  • A Config schema must not carry .default(...): on dsh 0.2.0-rc.2 that turned the merged row config into unresolved schema objects ({}) and silently dropped every configured value; the schema now declares plain types and the defaults live in readSettings()
  • Peer range widened to ^0.1.2-alpha.3 || ^0.1.7-rc.1 || ^0.2.0-rc.2 so one package serves both runtime lines

Changed

  • The service half creates a single TunnelManager shared by the /remote command and the HTTP routes

Known shell behaviours (not bugs)

  • A brand-new conversation shows no command card: the chat view renders the welcome surface until the session has model history, and command lifecycles are deliberately not model history. After the first turn every earlier card appears at once — the commands had already run
  • The status strip only renders once a session has content: it lives in the composer's dock seat, which the hero (empty-session) layout does not render. 0.2.1 adds a "Remote hosts" sidebar panel reachable in every session state

dsh v0.1.12

Choose a tag to compare

@github-actions github-actions released this 14 Sep 11:49

中文 | English

中文

问题修复

  • 修正 0.1.11 引入的本地隧道校验判据:当时要求一次性 token URL 返回 200,但 dsh 对有效 token 的应答是 303(种下浏览器 cookie 并重定向到干净 URL),已在同一会话中持有 cookie 时才是 200。要求"恰好 200"导致每一个有效本地端口都被判为"不是我们的 dsh":up 依次试完 5 个候选端口后报错 local port N did not serve our dsh (HTTP 303)。现在 2xx/3xx 都算"隧道通到我们自己的 dsh",只有 401/403(别的实例拒绝了我们的 token)才判定该候选端口不合格
  • 已在真实服务器 + dsh 0.1.5-rc.2 上验证:token URL 经隧道返回 303,浏览器跟随重定向后拿到完整 UI(HTTP 200,约 27 KB)

English

Bug Fixes

  • Corrects the local-tunnel criterion added in 0.1.11: it required HTTP 200 from the one-time token URL, but dsh answers a valid token with 303 (minting the browser cookie and redirecting to the clean URL) and only returns 200 when the cookie is already present. Demanding exactly 200 rejected every valid local port — up tried all five candidates and failed with local port N did not serve our dsh (HTTP 303). Any 2xx/3xx now means "this tunnel reaches our own dsh"; only 401/403 (another instance rejecting our token) disqualifies a candidate
  • Verified on a real server running dsh 0.1.5-rc.2: the token URL answers 303 through the tunnel, and following the redirect lands on the full UI (HTTP 200, ~27 KB)

dsh v0.1.11

Choose a tag to compare

@github-actions github-actions released this 14 Sep 11:42

中文 | English

中文

问题修复

  • 多用户安全:端口"在监听"不再等于"我的服务起来了"。在无法写共享登记表的共用服务器上,两个账号可能分到同一个端口(各自登记表互不可见,且双方的占用探测都发生在对方 dsh 真正绑定之前)。随后:输家(后绑定者)的 dsh 因 EADDRINUSE 进入 systemd 崩溃重启循环,而 systemd 仍把这次启动报告为成功;那个端口实际是赢家的 dsh 在提供服务——于是输家的隧道指到了同事的 dsh web,能看到对方的会话和文件。现在按 PID 归属判定:监听该端口的进程 pid 必须等于本 unit 的 MainPID;若被其他进程占用(或监听套接字无法归属到本进程),一律按撞车处理并顺延到下一个空闲端口
  • 本地隧道同样要"证明确实是我的"。同一台机器上并发 up 时,两个进程可能选中同一个本地端口(探测与实际绑定是两步),输家的 ssh 绑定失败并静默重连,而"TCP 能连上"却被赢家的隧道满足——于是输家给出一个实际指向别人 dsh 的 URL。现在本地端口通过"用本实例的一次性 token URL 请求一次,必须 200"来验证;候选端口若服务的不是我们自己的 dsh,就放弃并换下一个空闲本地端口(最多 5 个,仍受 localWaitSeconds 约束)
  • 修复 remoteListeners 在真实服务器上永远返回空的问题:sh -c "ss -tlnp …" 的参数没有加引号,远程 shell 实际执行的是不带参数的 ss(列出所有套接字、无 users: 信息),解析结果为空。这导致 audit 的"监听者/属主"信息和 conflict 判定在真机上从未生效;现在能正确显示 pid 与进程名
  • up --port <n> 改为"优先使用":该端口已被别人占用时,自动改分配空闲端口并明确告知,而不是失败——更不会假装成功连到别人的服务上

其他变更

  • 撞车重试时不再重复尝试用户强制指定的端口,而是转为自动分配
  • mock ssh 补齐了几个"掩盖了这些 bug"的真实契约:ss 遵循 -l/-t 语义(不带参数不会返回 LISTEN 行)、实现 systemctl show -p MainPID --value、模拟"进程绑定失败但 systemd 仍报告启动成功"的单元状态
  • mock 的 services.json 改为加锁读写:两个账号的 shim 进程原本会互相覆盖条目,导致"重启时杀掉对方 mock 服务"(纯测试环境缺陷,表现为偶发失败)

测试

  • 新增"实验室服务器场景"集成测试(两个账号、无共享登记表、并发 up):两人必须各自在自己的端口上提供服务,并按各自隧道实际返回的页面校验
  • happy path 增加断言:端口监听者的 pid 必须等于本 unit 的 MainPID,归属判定不会被无声改坏

English

Bug Fixes

  • Multi-user safety: a listening port no longer proves our service is up. On a shared server without a writable shared registry two accounts could allocate the SAME port (each registry is private, and both bind probes ran before either dsh bound). The loser's dsh then crash-looped on EADDRINUSE while systemd still reported the start as successful, and the port answered from the WINNER's dsh — so the loser's tunnel pointed at a labmate's web UI, exposing that account's sessions and files. The port is now attributed by PID: the listener's pid must equal our unit's MainPID; a port held by another process (or by a socket whose owner cannot be named) is a collision and the plugin moves to the next free port
  • The local tunnel must also prove it is ours. Two concurrent up runs on one machine can pick the same local port (the probe and the ssh bind are separate steps); the loser's ssh failed to bind and quietly reconnected, while "the port answers TCP" was satisfied by the WINNER's tunnel — so the loser printed a URL that actually served someone else's dsh. The local port is now verified by requesting this instance's one-time token URL through it and requiring HTTP 200; a candidate that does not serve our own dsh is abandoned and the next free local port is tried (up to 5, still bounded by localWaitSeconds)
  • remoteListeners returned nothing at all on real servers: the sh -c "ss -tlnp …" argument was unquoted, so the remote shell ran a bare ss (every socket, no users: details) and the parse came back empty. audit's listener/owner columns and its conflict verdict were therefore always empty on real hosts; they now report the actual pid and process
  • up --port <n> treats the port as a preference: when another process already holds it, the plugin allocates a free port and says so, instead of failing — or, worse, reporting success against someone else's service

Chores

  • A bind-race retry drops a user-forced port instead of retrying the same one
  • Mock ssh mirrors the real contracts that hid these bugs: ss honours -l/-t (a bare ss yields no LISTEN rows), systemctl show -p MainPID --value is implemented, and a mock service that dies on bind leaves its unit inactive while the restart still returns success
  • The mock's services.json is written under an exclusive lock: two accounts' shim processes were clobbering each other's entries, which made a restart kill the other account's mock server (a mock-only artifact that showed up as flaky tests)

Tests

  • New integration test for the lab-server case (two accounts, no shared registry, concurrent up): both users must end up serving their own dsh on distinct ports, checked against the page each tunnel actually returns
  • The happy path asserts the listener pid equals the unit's MainPID, so attribution cannot silently regress

dsh v0.1.10

Choose a tag to compare

@github-actions github-actions released this 14 Sep 07:08

中文 | English

中文

问题修复

  • 修复真机上端口复用从未生效的问题:读取 unit 端口的命令是 grep -E '--port [0-9]+',GNU grep 会把以 -- 开头的模式当作选项而报错,导致端口读不到、每次 up 都重新分配端口(登记表因此一行行堆积)。现在模式前加了 -- 终止选项解析;同时让 mock 的 grep 复刻 GNU 的选项解析行为,这类"只有真机会犯"的 bug 无法再被测试放过。此后 up 会输出 reusing registered remote port N 并复用已登记端口
  • down 现在除了停止 unit 还会 disable 它:此前 enabled 的 unit 会在服务器重启后自己拉起,占用一个登记表已标记 released 的端口(audit 里的 ORPHAN 行就是这么来的)。up/provision 会重新 enable;--keep-service 行为不变
  • 登记表查询改为取最新一行:登记表是追加式的,同一端口的旧 released 行会遮蔽当前 in-use 行——表现为隧道明明在跑,status 却显示 released;audit --release <端口> 也可能改到历史行而非当前行
  • 心跳只刷新 in-use 行,历史 released 行不再被"刷新"成刚活跃过的样子

体验优化

  • up 把一次性启动地址(authUrl)写入本地状态文件,open 优先使用它:此后单独执行 dsh --profile remote open <host> 也能直接打开页面,而不是在 dsh web ≥ 0.1.2-rc 上收到 401
  • status(CLI 与 /remote status)在已知时显示 auth: 地址

其他变更

  • mock ssh 补齐 GNU grep 选项解析与登记表心跳规则,测试行为与真实服务器对齐

English

Bug Fixes

  • Port reuse never worked on real servers: the unit port probe ran grep -E '--port [0-9]+', and GNU grep parses that pattern as an option and fails, so the ExecStart port came back empty and every up allocated a fresh port (which is also why the registry grew one row per up). The pattern now follows --, and the mock's grep reproduces GNU option parsing so this class of bug cannot hide again. up reports reusing registered remote port N and keeps the registered port
  • down now disables the unit as well as stopping it: an enabled unit came back after a server reboot and re-occupied a port the registry had already marked released (the ORPHAN rows seen in audit). up/provision enable it again; --keep-service is unchanged
  • Registry lookups use the newest row for a port: the registry is append-ordered, so an older released row could shadow the live in-use row — status showed "released" while the tunnel was up, and audit --release <port> could rewrite history instead of the effective row
  • Heartbeats only refresh in-use rows, so historical released rows no longer look freshly alive

Improvements

  • up stores the one-time launch URL (authUrl) in the local state file and open prefers it, so a later dsh --profile remote open <host> reaches the page instead of answering 401 on dsh web ≥ 0.1.2-rc
  • status (CLI and /remote status) shows the auth: URL when one is known

Chores

  • The mock ssh mirrors GNU grep option parsing and the registry's in-use heartbeat rule, keeping the tests aligned with the real server

dsh v0.1.9

Choose a tag to compare

@github-actions github-actions released this 05 Sep 13:07

中文 | English

中文

体验优化

  • 新增 bootstrap <host> 子命令:按 ssh 登录账号一键补齐远程环境(Node/dsh 装进该账号的 /.npm-global、/.dsh、systemd linger、PATH 条目),幂等——同一个主机每人各自的账号在自己电脑上跑一次即可;--upgrade 同时解决"以后怎么更新远程 dsh"
  • 远程 dsh 发现新增 ~/.npm-global/bin/dsh 兜底探测,即使某个账号的 ssh 通道 PATH 不含 npm-global 也能正常识别与启动

问题修复

  • check 改用探测到的绝对路径执行 node/dsh 版本检查(不再依赖 PATH 里有没有裸命令名),缺失提示与 up/provision 报错统一指向 bootstrap

其他变更

  • /remote 斜杠命令新增 bootstrap;README 双语同步多账号使用流程(各自账号、各自会话、各自端口)

English

Improvements

  • New bootstrap <host> subcommand installs the remote prerequisites for the account the ssh alias logs in as (Node, dsh into that account's ~/.npm-global, ~/.dsh, systemd linger, PATH entries) — idempotent, one run per labmate's own user; --upgrade also solves "how do I update remote dsh"
  • Remote dsh discovery now falls back to probing ~/.npm-global/bin/dsh, so accounts whose ssh-channel PATH omits the npm-global dir resolve dsh correctly

Bug Fixes

  • check probes node/dsh via the discovered absolute paths instead of bare command names; missing-tool errors in up/provision and check hints now point at bootstrap

Chores

  • /remote gains bootstrap; READMEs (EN/ZH) document the per-account multi-user flow

dsh v0.1.8

Choose a tag to compare

@github-actions github-actions released this 03 Sep 11:38

中文 | English

中文

问题修复

  • 修复 up 抓取 token URL 的竞态:dsh web 启动后 2-4 秒才把带 token 的 URL 打印到 journal,而且旧版本残留的「无 token」URL 行还在 200 行窗口里——现在只认最后一次服务启动之后的最后一条带 ?token= 的 URL,并自动重试等待打印(4 次 × 1.5 秒)

English

Bug Fixes

  • Fix the race when up fetches the one-time-token launch URL: dsh web prints it 2-4s AFTER systemd reports "Started", and stale pre-token URL lines from older versions still sit inside the 200-line window — now only the last ?token= URL after the most recent unit start counts, with retries spanning the print delay

dsh v0.1.7

Choose a tag to compare

@github-actions github-actions released this 03 Sep 10:22

中文

问题修复

  • up 现在会自动抓取 dsh web 的启动 URL:dsh web ≥ 0.1.2-rc 用启动时打印的一次性 token 鉴权,插件从 unit journal 里取出该 URL 并改写成本地隧道端口,直接打印带 token 的地址(auth: 行);up --open 也打开带 token 的 URL
  • 抓取失败时退化为提示 logs <host> 查看

其他变更

  • README 双语新增 token 鉴权相关排障说明

English

Bug Fixes

  • up now surfaces the dsh web launch URL: since dsh web 0.1.2-rc gates its UI behind a one-time token printed at startup, the plugin fetches the unit journal and prints the token URL rewritten to the local tunnel port (auth: line); up --open opens that URL as well
  • Falls back to pointing at logs <host> when the journal cannot supply it

Chores

  • READMEs document the new token flow

dsh v0.1.6

Choose a tag to compare

@Linjiangxian0203 Linjiangxian0203 released this 01 Sep 11:04

中文

问题修复

  • 配置文件解析失败改走标准错误通道(E_CONFIG),CLI 按熟悉的格式化样式输出并带提示
  • web profile 判定不再嗅探 process.argv,只信任宿主注入的 webStartup 上下文——改名/复制的 web profile 也能正确路由

其他变更

  • 路线图收尾,插件转入维护模式:dsh 升级时做兼容性核查,按需修复

English

Bug Fixes

  • Config parse failures now use the standard error path (E_CONFIG), so the CLI prints the familiar formatted message with hints
  • Web-profile detection no longer sniffs process.argv; it trusts the host's webStartup context, so renamed or copied web profiles route correctly

Chores

  • Roadmap complete — the plugin enters maintenance mode: compatibility checks on dsh upgrades, fixes on demand

dsh v0.1.5

Choose a tag to compare

@Linjiangxian0203 Linjiangxian0203 released this 01 Sep 10:45

中文

体验优化

  • audit --clean-stale 一次 flock+awk 批量改写全部 stale 行(N 行 = 1 次 ssh 往返),保持 mktemp + 原地写语义(gawk/mawk 双实测)
  • 本地端口耗尽诊断:整个区间只跑一次 netstat + 一次 tasklist(POSIX 一次 lsof),不再每端口起进程
  • peer/dev 依赖对齐 @deepseek-ai/dsh-cmdline ^0.1.2-alpha.3,当前 harness alpha 下不再有 UNMET PEER 警告

其他变更

  • npm 包不再携带 test/;新增英文版登记表格式文档;精简 README 过时排障行

English

Improvements

  • audit --clean-stale rewrites every stale row in one flock+awk pass (N rows = one ssh round-trip), keeping the mktemp + in-place write semantics (verified on gawk and mawk)
  • Local port-exhaustion diagnostics use one netstat + one tasklist (or one lsof) for the whole range instead of a spawn per port
  • Peer/dev aligned to @deepseek-ai/dsh-cmdline ^0.1.2-alpha.3 — no more UNMET PEER warning on current harness alphas

Chores

  • test/ no longer shipped in the npm package; added an English registry-format doc; refreshed an outdated README troubleshooting row