dsh-remote 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/README.zh.md +81 -0
- package/lib/client.js +121 -0
- package/lib/index.js +124 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-remote contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# dsh-remote
|
|
2
|
+
|
|
3
|
+
Remote-access assistant for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).
|
|
4
|
+
|
|
5
|
+
The harness Web GUI intentionally listens on `127.0.0.1` only — the CLI rejects `--host 0.0.0.0` for safety (the GUI has no auth and the agent executes code). That means remote access is a **tunneling workflow**, and this plugin makes it copy-paste easy:
|
|
6
|
+
|
|
7
|
+
- **`/remote` slash command** — prints the exact commands for this instance
|
|
8
|
+
- **Settings → 远程访问 (Remote Access)** — live port, LAN IPs, trusted hosts and command blocks with copy buttons
|
|
9
|
+
- Covers the three supported paths: SSH local forward (`ssh -L`), keepalive (`autossh`), reverse tunnel (`ssh -R`, NAT-friendly), and reverse-proxy access with `--trusted-host`
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install dsh-remote
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Add a row to your profile `cordis.yml` (or `cordis.patch.yml`):
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
- id: dsh-remote
|
|
21
|
+
name: dsh-remote
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### From the chat
|
|
27
|
+
|
|
28
|
+
Type `/remote` (optionally `/remote user@host`):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
/remote root@9.134.186.191
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Output:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
## dsh-remote — remote access
|
|
38
|
+
|
|
39
|
+
Web GUI: http://127.0.0.1:3080
|
|
40
|
+
|
|
41
|
+
### Tunnel from your PC (recommended — harness stays loopback)
|
|
42
|
+
ssh -N -L 18080:127.0.0.1:3080 root@9.134.186.191
|
|
43
|
+
autossh -M 0 -N -L 18080:127.0.0.1:3080 root@9.134.186.191
|
|
44
|
+
|
|
45
|
+
### Reverse tunnel (remote pushes out; NAT-friendly)
|
|
46
|
+
ssh -N -R 3080:127.0.0.1:3080 root@9.134.186.191
|
|
47
|
+
|
|
48
|
+
### Direct web access behind a reverse proxy (add auth yourself!)
|
|
49
|
+
dsh --profile web --port 3080 --trusted-host <your-public-host>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Run the `ssh -N -L ...` line on your PC, then open **http://127.0.0.1:18080** in the browser. Because the browser origin stays loopback, the harness `/api` trust fence accepts it with no extra flags — and the connection is SSH-encrypted and authenticated.
|
|
53
|
+
|
|
54
|
+
### From the settings page
|
|
55
|
+
|
|
56
|
+
Settings → 远程访问 shows the same information, plus the machine's LAN IPs, with one-click copy per command. Entering `user@host` regenerates the commands with that target.
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
| Key | Type | Default | Meaning |
|
|
61
|
+
| --- | --- | --- | --- |
|
|
62
|
+
| `localPort` | int | 18080 | Local port used by the generated `ssh -L` / `autossh` commands (the PC side) |
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- id: dsh-remote
|
|
66
|
+
name: dsh-remote
|
|
67
|
+
config:
|
|
68
|
+
localPort: 20080
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Why not just bind 0.0.0.0?
|
|
72
|
+
|
|
73
|
+
The harness deliberately refuses `--host 0.0.0.0` (`error: --host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network`). The GUI has no authentication, and the agent can execute code in the sandbox — binding every interface would hand remote code execution to anyone on the network. Tunnels (SSH or an authenticated reverse proxy) are the supported way, and that is what this plugin automates.
|
|
74
|
+
|
|
75
|
+
## Browser page
|
|
76
|
+
|
|
77
|
+
Settings → 远程访问 (Remote Access). The page fetches `GET /dsh-remote/info` from the harness `webServer` service (same-origin JSON route).
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# dsh-remote
|
|
2
|
+
|
|
3
|
+
为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)打造的**远程接入助手**插件。
|
|
4
|
+
|
|
5
|
+
Harness 的 Web GUI **刻意只监听 `127.0.0.1`**——CLI 直接拒绝 `--host 0.0.0.0`(安全设计:GUI 无鉴权、Agent 能执行代码,绑全网等于把远程代码执行暴露给网络)。所以远程访问本质上是个**隧道工作流**,这个插件把它变成"复制粘贴":
|
|
6
|
+
|
|
7
|
+
- **`/remote` 斜杠命令** —— 直接打印本实例的精确隧道命令
|
|
8
|
+
- **设置 → 远程访问** —— 实时端口、局域网 IP、trusted hosts 与带一键复制的命令块
|
|
9
|
+
- 覆盖三条官方支持的路径:SSH 本地转发(`ssh -L`)、保活(`autossh`)、反向隧道(`ssh -R`,NAT 友好)、反向代理直达(`--trusted-host`)
|
|
10
|
+
|
|
11
|
+
## 安装
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install dsh-remote
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
在你的 profile `cordis.yml`(或 `cordis.patch.yml`)里加一行:
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
- id: dsh-remote
|
|
21
|
+
name: dsh-remote
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 用法
|
|
25
|
+
|
|
26
|
+
### 聊天里
|
|
27
|
+
|
|
28
|
+
输入 `/remote`(可选带 `/remote user@host`):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
/remote root@9.134.186.191
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
输出:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
## dsh-remote — remote access
|
|
38
|
+
|
|
39
|
+
Web GUI: http://127.0.0.1:3080
|
|
40
|
+
|
|
41
|
+
### Tunnel from your PC (recommended — harness stays loopback)
|
|
42
|
+
ssh -N -L 18080:127.0.0.1:3080 root@9.134.186.191
|
|
43
|
+
autossh -M 0 -N -L 18080:127.0.0.1:3080 root@9.134.186.191
|
|
44
|
+
|
|
45
|
+
### Reverse tunnel (remote pushes out; NAT-friendly)
|
|
46
|
+
ssh -N -R 3080:127.0.0.1:3080 root@9.134.186.191
|
|
47
|
+
|
|
48
|
+
### Direct web access behind a reverse proxy (add auth yourself!)
|
|
49
|
+
dsh --profile web --port 3080 --trusted-host <your-public-host>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
在你 PC 上跑 `ssh -N -L ...` 那行,然后浏览器打开 **http://127.0.0.1:18080**。因为浏览器源仍是 loopback,harness 的 `/api` 信任围栏直接放行、无需任何额外参数——且连接走 SSH 加密认证。
|
|
53
|
+
|
|
54
|
+
### 设置页
|
|
55
|
+
|
|
56
|
+
设置 → 远程访问:同样的信息 + 本机局域网 IP,每个命令一键复制;输入 `user@host` 可按目标重新生成命令。
|
|
57
|
+
|
|
58
|
+
## 配置
|
|
59
|
+
|
|
60
|
+
| 键 | 类型 | 默认 | 含义 |
|
|
61
|
+
| --- | --- | --- | --- |
|
|
62
|
+
| `localPort` | int | 18080 | 生成的 `ssh -L` / `autossh` 命令里的本机端口(PC 侧) |
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- id: dsh-remote
|
|
66
|
+
name: dsh-remote
|
|
67
|
+
config:
|
|
68
|
+
localPort: 20080
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 为什么不能直接绑 0.0.0.0?
|
|
72
|
+
|
|
73
|
+
Harness 刻意拒绝 `--host 0.0.0.0`(报错原文:`intentionally not supported yet for safety: it would expose remote code execution to the network`)。GUI 没有登录鉴权,而 Agent 能在沙箱里执行代码——绑全网 = 把远程代码执行交给网段里任何人。隧道(SSH 或带鉴权的反向代理)才是官方支持的方式,这正是本插件自动化的东西。
|
|
74
|
+
|
|
75
|
+
## 浏览器页面
|
|
76
|
+
|
|
77
|
+
设置 → 远程访问。页面通过 harness 官方 `webServer` 服务拉取 `GET /dsh-remote/info`(同源 JSON 路由)。
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// dsh-remote — remote-access assistant for DeepSeek Harness (Client half).
|
|
2
|
+
//
|
|
3
|
+
// Settings → 远程访问 (Remote Access): shows the live port, LAN IPs, trusted
|
|
4
|
+
// hosts and the exact SSH tunnel / reverse-tunnel / reverse-proxy commands,
|
|
5
|
+
// with a copy button. Data comes from the host half's JSON endpoint
|
|
6
|
+
// (GET /dsh-remote/info) served by the official `webServer` service.
|
|
7
|
+
import * as React from 'react'
|
|
8
|
+
|
|
9
|
+
export const name = 'dsh-remote'
|
|
10
|
+
|
|
11
|
+
async function fetchInfo(target) {
|
|
12
|
+
const q = target ? '?target=' + encodeURIComponent(target) : ''
|
|
13
|
+
const res = await fetch('/dsh-remote/info' + q)
|
|
14
|
+
const data = await res.json().catch(() => ({}))
|
|
15
|
+
if (!res.ok) throw new Error((data && data.error) || 'HTTP ' + res.status)
|
|
16
|
+
return data
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function apply(ctx) {
|
|
20
|
+
const slots = ctx.get('slots')
|
|
21
|
+
if (slots === undefined) return
|
|
22
|
+
|
|
23
|
+
function RemotePage() {
|
|
24
|
+
const [info, setInfo] = React.useState(null)
|
|
25
|
+
const [error, setError] = React.useState('')
|
|
26
|
+
const [target, setTarget] = React.useState('')
|
|
27
|
+
const [copied, setCopied] = React.useState(false)
|
|
28
|
+
|
|
29
|
+
const refresh = (t) => {
|
|
30
|
+
fetchInfo(t || '')
|
|
31
|
+
.then(setInfo)
|
|
32
|
+
.catch((err) => setError(String((err && err.message) || err)))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
refresh('')
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
const copy = (text) => {
|
|
40
|
+
if (navigator && navigator.clipboard) {
|
|
41
|
+
navigator.clipboard
|
|
42
|
+
.writeText(text)
|
|
43
|
+
.then(() => {
|
|
44
|
+
setCopied(true)
|
|
45
|
+
setTimeout(() => setCopied(false), 1500)
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const block = (title, code) =>
|
|
52
|
+
React.createElement(
|
|
53
|
+
'div',
|
|
54
|
+
{ key: title, style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
55
|
+
React.createElement('div', { style: { fontSize: '13px', fontWeight: 600 } }, title),
|
|
56
|
+
React.createElement('code', {
|
|
57
|
+
style: {
|
|
58
|
+
display: 'block',
|
|
59
|
+
padding: '8px 10px',
|
|
60
|
+
borderRadius: '6px',
|
|
61
|
+
border: '1px solid rgba(128,128,128,0.35)',
|
|
62
|
+
fontSize: '12px',
|
|
63
|
+
whiteSpace: 'pre-wrap',
|
|
64
|
+
wordBreak: 'break-all',
|
|
65
|
+
},
|
|
66
|
+
}, code),
|
|
67
|
+
React.createElement('button', { onClick: () => copy(code), style: { alignSelf: 'flex-start' } }, '复制'),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if (error)
|
|
71
|
+
return React.createElement('div', { style: { color: '#e06c75', padding: 16 } }, error)
|
|
72
|
+
if (!info)
|
|
73
|
+
return React.createElement('div', { style: { padding: 16, opacity: 0.6 } }, '加载中…')
|
|
74
|
+
|
|
75
|
+
const c = info.commands || {}
|
|
76
|
+
const lan = Array.isArray(info.lanAddresses) ? info.lanAddresses : []
|
|
77
|
+
const trusted = Array.isArray(info.trustedHosts) ? info.trustedHosts : []
|
|
78
|
+
|
|
79
|
+
return React.createElement(
|
|
80
|
+
'div',
|
|
81
|
+
{ style: { padding: '16px', display: 'flex', flexDirection: 'column', gap: '14px' } },
|
|
82
|
+
React.createElement('div', { style: { fontWeight: 600 } }, '远程访问(dsh-remote)'),
|
|
83
|
+
React.createElement(
|
|
84
|
+
'div',
|
|
85
|
+
{ style: { display: 'flex', gap: '16px', flexWrap: 'wrap', fontSize: '13px' } },
|
|
86
|
+
React.createElement('span', null, 'Web GUI: ', React.createElement('code', null, info.url)),
|
|
87
|
+
React.createElement('span', null, '端口: ', React.createElement('code', null, String(info.port))),
|
|
88
|
+
lan.length
|
|
89
|
+
? React.createElement('span', null, '局域网: ', React.createElement('code', null, lan.join(', ')))
|
|
90
|
+
: null,
|
|
91
|
+
trusted.length
|
|
92
|
+
? React.createElement('span', null, 'trusted hosts: ', React.createElement('code', null, trusted.join(', ')))
|
|
93
|
+
: null,
|
|
94
|
+
),
|
|
95
|
+
React.createElement('div', { style: { fontSize: '12px', opacity: 0.75 } },
|
|
96
|
+
'跨机器访问请用 SSH 隧道。harness 默认只监听 127.0.0.1,这是官方安全设计(GUI 无鉴权,--host 0.0.0.0 被刻意拒绝)。可选:输入 user@host 生成具体命令。',
|
|
97
|
+
),
|
|
98
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px' } },
|
|
99
|
+
React.createElement('input', {
|
|
100
|
+
value: target,
|
|
101
|
+
onChange: (ev) => setTarget(ev.target.value),
|
|
102
|
+
placeholder: 'user@remote-host(可选)',
|
|
103
|
+
style: { flex: 1 },
|
|
104
|
+
}),
|
|
105
|
+
React.createElement('button', { onClick: () => refresh(target) }, '生成'),
|
|
106
|
+
),
|
|
107
|
+
c.localForward ? block('本机 → 远程(ssh 本地转发,推荐)', c.localForward) : null,
|
|
108
|
+
c.localForwardAuto ? block('autossh 保活版本', c.localForwardAuto) : null,
|
|
109
|
+
c.reverseForward ? block('远程 → 本机(反向隧道,NAT 友好)', c.reverseForward) : null,
|
|
110
|
+
c.trustedFlag ? block('反向代理直达(需自行加鉴权)', c.trustedFlag) : null,
|
|
111
|
+
copied ? React.createElement('div', { style: { fontSize: '12px', opacity: 0.7 } }, '已复制 ✓') : null,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
slots.inject('settings.section', () =>
|
|
116
|
+
slots.register(
|
|
117
|
+
{ name: 'settings.section', id: 'dsh-remote', order: 40, label: () => '远程访问' },
|
|
118
|
+
() => React.createElement(RemotePage, null),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// dsh-remote — remote-access assistant for DeepSeek Harness (Host half).
|
|
2
|
+
//
|
|
3
|
+
// The harness Web server intentionally binds loopback only (the CLI rejects
|
|
4
|
+
// --host 0.0.0.0 for safety: the GUI has no auth and the agent executes code).
|
|
5
|
+
// This plugin does not fight that design — it makes the supported paths easy:
|
|
6
|
+
// • a `/remote` slash command that prints the exact tunnel commands,
|
|
7
|
+
// • a JSON endpoint (GET /dsh-remote/info) the settings page consumes.
|
|
8
|
+
// Commands cover local-forward (ssh -L), keepalive (autossh), reverse tunnel
|
|
9
|
+
// (ssh -R, NAT-friendly) and reverse-proxy (with --trusted-host) guidance.
|
|
10
|
+
import { networkInterfaces } from 'node:os'
|
|
11
|
+
import z from '@deepseek-ai/schemastery'
|
|
12
|
+
|
|
13
|
+
export const name = 'dsh-remote'
|
|
14
|
+
|
|
15
|
+
// The web server is the hard dependency: its live port feeds every command.
|
|
16
|
+
export const inject = ['webServer']
|
|
17
|
+
|
|
18
|
+
/** Optional deployment tuning. */
|
|
19
|
+
export const Config = z.object({
|
|
20
|
+
/** Local port used by the ssh -L / autossh commands generated for the PC side. */
|
|
21
|
+
localPort: z.number().int().min(1).max(65535).default(18080),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/** Non-loopback IPv4 addresses of this machine (empty when none are up). */
|
|
25
|
+
function lanAddresses() {
|
|
26
|
+
const out = []
|
|
27
|
+
for (const addrs of Object.values(networkInterfaces())) {
|
|
28
|
+
for (const addr of addrs ?? []) {
|
|
29
|
+
if (addr.family === 'IPv4' && !addr.internal) out.push(addr.address)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function apply(ctx, config) {
|
|
36
|
+
const port = ctx.webServer.port
|
|
37
|
+
const url = `http://127.0.0.1:${port}`
|
|
38
|
+
const lan = lanAddresses()
|
|
39
|
+
|
|
40
|
+
const webStartup = ctx.get('webStartup')
|
|
41
|
+
const trustedHosts =
|
|
42
|
+
webStartup && Array.isArray(webStartup.trustedHosts) ? webStartup.trustedHosts : []
|
|
43
|
+
|
|
44
|
+
const buildCommands = (target) => {
|
|
45
|
+
const remote = target && target.trim() ? target.trim() : '<user@remote-host>'
|
|
46
|
+
return {
|
|
47
|
+
localForward: `ssh -N -L ${config.localPort}:127.0.0.1:${port} ${remote}`,
|
|
48
|
+
localForwardAuto: `autossh -M 0 -N -L ${config.localPort}:127.0.0.1:${port} ${remote}`,
|
|
49
|
+
reverseForward: `ssh -N -R ${port}:127.0.0.1:${port} ${remote}`,
|
|
50
|
+
trustedFlag: `dsh --profile web --port ${port} --trusted-host <your-public-host>`,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const info = (target) => ({
|
|
55
|
+
port,
|
|
56
|
+
url,
|
|
57
|
+
lanAddresses: lan,
|
|
58
|
+
trustedHosts,
|
|
59
|
+
commands: buildCommands(target || ''),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// ── /remote slash command ────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const commands = ctx.get('commands')
|
|
65
|
+
if (commands !== undefined) {
|
|
66
|
+
commands.register({
|
|
67
|
+
name: 'remote',
|
|
68
|
+
description:
|
|
69
|
+
'Show remote-access tunnel commands for this harness instance. Optional argument: user@host for the ssh tunnel target.',
|
|
70
|
+
handler: (invocation) => {
|
|
71
|
+
const target = (invocation.rawInput || '').trim()
|
|
72
|
+
const i = info(target)
|
|
73
|
+
const remote = target || '<user@remote-host>'
|
|
74
|
+
const lines = [
|
|
75
|
+
'## dsh-remote — remote access',
|
|
76
|
+
'',
|
|
77
|
+
`Web GUI: ${i.url}`,
|
|
78
|
+
...(lan.length ? [`LAN IPs: ${lan.join(', ')}`] : []),
|
|
79
|
+
...(trustedHosts.length ? [`trusted hosts: ${trustedHosts.join(', ')}`] : []),
|
|
80
|
+
'',
|
|
81
|
+
'### Tunnel from your PC (recommended — harness stays loopback)',
|
|
82
|
+
`ssh -N -L ${config.localPort}:127.0.0.1:${port} ${remote}`,
|
|
83
|
+
`autossh -M 0 -N -L ${config.localPort}:127.0.0.1:${port} ${remote}`,
|
|
84
|
+
'',
|
|
85
|
+
'### Reverse tunnel (remote pushes out; NAT-friendly)',
|
|
86
|
+
`ssh -N -R ${port}:127.0.0.1:${port} ${remote}`,
|
|
87
|
+
'',
|
|
88
|
+
'### Direct web access behind a reverse proxy (add auth yourself!)',
|
|
89
|
+
`dsh --profile web --port ${port} --trusted-host <your-public-host>`,
|
|
90
|
+
'',
|
|
91
|
+
'Note: --host 0.0.0.0 is intentionally rejected by the harness for safety.',
|
|
92
|
+
]
|
|
93
|
+
return { kind: 'text', text: lines.join('\n') }
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── JSON endpoint for the settings page ──────────────────────────────────
|
|
99
|
+
|
|
100
|
+
const webServer = ctx.get('webServer')
|
|
101
|
+
if (webServer === undefined) return
|
|
102
|
+
const sendJson = (res, status, body) => {
|
|
103
|
+
res.statusCode = status
|
|
104
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
|
105
|
+
res.end(JSON.stringify(body))
|
|
106
|
+
}
|
|
107
|
+
const disposeRoute = webServer.register({
|
|
108
|
+
kind: 'exact',
|
|
109
|
+
path: '/dsh-remote/info',
|
|
110
|
+
handler: async (req, res) => {
|
|
111
|
+
const query = (req.url || '').split('?')[1] || ''
|
|
112
|
+
const match = query.match(/target=([^&]*)/)
|
|
113
|
+
let target = ''
|
|
114
|
+
try {
|
|
115
|
+
target = match ? decodeURIComponent(match[1]) : ''
|
|
116
|
+
} catch {
|
|
117
|
+
target = ''
|
|
118
|
+
}
|
|
119
|
+
sendJson(res, 200, info(target))
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
// Route is not fiber-bound: retain the disposer so stop/update never leaks.
|
|
123
|
+
ctx.effect(() => disposeRoute, 'dsh-remote.routeDispose')
|
|
124
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-remote",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Remote-access assistant for DeepSeek Harness: /remote command and a settings page that print the exact SSH tunnel / reverse-tunnel / reverse-proxy commands for this instance (harness intentionally binds loopback only).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"deepseek-harness",
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"remote",
|
|
10
|
+
"ssh",
|
|
11
|
+
"tunnel",
|
|
12
|
+
"plugin"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./lib/index.js",
|
|
18
|
+
"./client": "./lib/client.js",
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dsh": {
|
|
26
|
+
"client": {
|
|
27
|
+
"platform": "web"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
|
|
32
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
33
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
34
|
+
}
|
|
35
|
+
}
|