ds-harness-remote 0.4.7 → 0.4.9
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/README.md +65 -170
- package/dist/client-runtime.d.ts +4 -0
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js +105 -47
- package/dist/client-runtime.js.map +1 -1
- package/dist/client.github.js +166 -121
- package/dist/client.js +166 -121
- package/dist/client.js.map +1 -1
- package/dist/index.js +97 -44
- package/dist/index.js.map +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dsh-plugin.json +1 -1
- package/package.json +18 -2
package/README.md
CHANGED
|
@@ -1,207 +1,102 @@
|
|
|
1
|
-
#
|
|
1
|
+
# DeepSeek Harness Remote
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Continue your DeepSeek Harness sessions and experimental Codex workspaces from another device over an end-to-end encrypted connection.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[GitHub](https://github.com/liguobao/ds-harness-remote) · [Full guide](https://github.com/liguobao/ds-harness-remote#readme) · [中文说明](https://github.com/liguobao/ds-harness-remote/blob/main/README.zh.md) · [Remote Web](https://dsh.r2049.cn/app) · [Android](https://github.com/liguobao/ds-harness-remote/releases/latest)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`ds-harness-remote` is the Remote Host and workspace plugin for DSH. Harness keeps running on your work computer with its existing workspaces, tools, and permission controls; Remote gives your authorized devices another window into that environment.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
> Install this package with `dsh plugin`, not `npm install`. The DSH command updates the selected profile and adds the required bundle configuration.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
> selected profile and adds the bundle configuration layer.
|
|
13
|
-
>
|
|
14
|
-
> 这是 DSH bundle,请通过 `dsh plugin` 安装,不要直接使用 npm。DSH 命令会更新指定 profile,
|
|
15
|
-
> 并加入 bundle 配置层。
|
|
11
|
+
## Highlights
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
- Continue active sessions and review progress from another computer, the web, or Android
|
|
14
|
+
- Send text and image prompts, answer questions, and handle permission requests
|
|
15
|
+
- Open workspaces on another authorized computer without replacing the native Harness interface
|
|
16
|
+
- Reach the Host without opening a public listening port or configuring router port forwarding
|
|
17
|
+
- Protect session traffic with authenticated end-to-end encryption
|
|
18
|
+
- Preview remote files through the optional, read-only `dsh-file-viewer` integration
|
|
19
|
+
- Open Host Codex projects in the existing Remote UI through the optional experimental Codex domain
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
## Install
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
### DSH Desktop
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
2. Sign in with GitHub or Zhihu QR authorization, or with your account and password. New password accounts can register through [Remote Web](https://dsh.r2049.cn/app/register); the site shows the current invitation requirements.
|
|
25
|
-
3. Enable remote control for the current computer, or select another online device to control it directly.
|
|
26
|
-
4. Enter an existing workspace or browse remote directories to open one. Use **Exit** to return local and stop forwarding.
|
|
25
|
+
[DSH Desktop](https://github.com/liguobao/dsh-desktop) includes Remote and enables it by default. No separate plugin installation is required.
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
2. 使用 GitHub/知乎扫码授权登录,或使用账号密码登录。新的账号密码用户可从 [Remote Web](https://dsh.r2049.cn/app/register) 注册,当前邀请要求以站点页面为准。
|
|
30
|
-
3. 为当前机器启用远端控制,或直接选择另一台在线设备并控制它。
|
|
31
|
-
4. 进入已有 Workspace,或浏览远端目录后打开 Workspace;使用 **退出** 回到本地并停止转发。
|
|
27
|
+
### Existing DSH installation
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
Add the exact package version to the `web` profile, then restart Harness:
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
512 KiB transfer chunks; image preprocessing, DeepSeek Files API upload, and
|
|
39
|
-
file-id reuse stay on the Host in the official adapter.
|
|
40
|
-
|
|
41
|
-
使用 DSH `dsh-v0.1.1-rc.2` 或 `dsh-v0.1.2-alpha.1`–`rc.1` 时,原生会话 UI 可通过
|
|
42
|
-
`session.prompt` 发送图片,并通过只读 `session.attachment` 回读显示。较大的 ApiProxy 或
|
|
43
|
-
Gateway envelope 使用受限的 512 KiB 分块;图片预处理、DeepSeek Files API 上传与 file-id
|
|
44
|
-
复用仍由 Host 官方 adapter 完成。
|
|
45
|
-
|
|
46
|
-
## Directory browsing / 目录浏览
|
|
47
|
-
|
|
48
|
-
- Calls remote `host.listDirectory` first.
|
|
49
|
-
- Returns read-only metadata only: one-level children, absolute path, breadcrumbs, Home, hidden flag.
|
|
50
|
-
- The picker does not read file contents and never writes or mutates directories.
|
|
51
|
-
|
|
52
|
-
- 默认先调用远端 `host.listDirectory`。
|
|
53
|
-
- 仅返回只读元数据:单层子目录、绝对路径、面包屑、Home、隐藏标记。
|
|
54
|
-
- Workspace 选择器不读取文件内容,也不改文件系统、不建/改/删目录。
|
|
55
|
-
|
|
56
|
-
## Remote File Viewer / 远端文件查看
|
|
57
|
-
|
|
58
|
-
Install `dsh-file-viewer` on the Host and Client profiles to reuse its in-app
|
|
59
|
-
renderers for remote workspace files. Remote exposes only `stat`, bounded
|
|
60
|
-
`readRange`, and directory `list` over the authenticated encrypted channel.
|
|
61
|
-
Each transport read is capped at 512 KiB and larger viewer reads are assembled
|
|
62
|
-
from multiple chunks. Provider root/locator authorization remains in File Viewer.
|
|
63
|
-
|
|
64
|
-
在 Host 与 Client profile 中安装 `dsh-file-viewer` 后,可复用其内置渲染器查看远端
|
|
65
|
-
Workspace 文件。Remote 仅在认证加密通道中开放 `stat`、受限 `readRange` 与目录 `list`;
|
|
66
|
-
每个传输分块最多 512 KiB,更大的读取由客户端自动拼接。根目录与 locator 权限继续由
|
|
67
|
-
File Viewer provider 校验。
|
|
68
|
-
|
|
69
|
-
Remote preview never exposes write/delete/upload/execute, `openExternal`, or a
|
|
70
|
-
general filesystem RPC.
|
|
31
|
+
```sh
|
|
32
|
+
dsh plugin --profile web add ds-harness-remote@0.4.9
|
|
33
|
+
```
|
|
71
34
|
|
|
72
|
-
|
|
35
|
+
### dsh-TUI Host
|
|
73
36
|
|
|
74
|
-
|
|
37
|
+
Remote can also run as a Host in a terminal-only [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) profile:
|
|
75
38
|
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
-> ApiProxySwitch (rc.2) / TypertGatewaySwitch (alpha)
|
|
79
|
-
-> RemoteHarnessApiProxy / RemoteTypertGateway
|
|
80
|
-
-> Adaptive transport (LAN / P2P / TURN / Relay)
|
|
81
|
-
-> Noise IK secure channel
|
|
82
|
-
-> HarnessApiBridge / HarnessRemoteBridge allowlist
|
|
83
|
-
-> Remote Harness ApiProxy or TypertGateway / FileViewerHost read-only bridge
|
|
39
|
+
```sh
|
|
40
|
+
dsh plugin --profile dsh-tui add ds-harness-remote@0.4.9
|
|
84
41
|
```
|
|
85
42
|
|
|
43
|
+
After starting dsh-TUI, manage Remote with its native slash command:
|
|
44
|
+
|
|
86
45
|
```text
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
-> HarnessApiBridge / HarnessRemoteBridge 白名单
|
|
93
|
-
-> 远端 Harness ApiProxy 或 TypertGateway / FileViewerHost 只读桥
|
|
46
|
+
/remote
|
|
47
|
+
/remote login
|
|
48
|
+
/remote login github
|
|
49
|
+
/remote status
|
|
50
|
+
/remote logout
|
|
94
51
|
```
|
|
95
52
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
- `service.ts`: Host lifecycle / Host 生命周期
|
|
99
|
-
- `client-runtime.ts`: device list, connection, workspace actions / 设备列表、连接、工作区操作
|
|
100
|
-
- `client.ts`: settings UI, remote modal, sidebar, remote header / 设置卡片、Remote 弹窗、侧边栏、远端 Header
|
|
101
|
-
- `harness-api-bridge.ts`: ApiProxy allowlist and stream bridge / ApiProxy 白名单与 stream 桥接
|
|
102
|
-
- `harness-remote-bridge.ts`: v0.1.2 Typert Remote allowlist and stream carrier / v0.1.2 Typert Remote 白名单与 stream carrier
|
|
103
|
-
- `file-viewer-bridge.ts`: bounded File Viewer read bridge / 受限 File Viewer 读取桥
|
|
104
|
-
- `identity-store.ts` / `server-credentials.ts`: account-scoped identity and credentials / 账号隔离的身份与凭证
|
|
105
|
-
|
|
106
|
-
## Security model / 安全模型
|
|
107
|
-
|
|
108
|
-
- The Harness Host does not require a public listening port. Clients can connect from anywhere with internet access over a bidirectional end-to-end encrypted channel.
|
|
109
|
-
- Host/Client authenticate with long-lived X25519 keys using Noise IK.
|
|
110
|
-
- ApiProxy methods and Typert Remote endpoints are allowlist-driven (fail-closed).
|
|
111
|
-
- Workspace-picker browsing is metadata-only. Optional File Viewer preview is read-only, bounded, and provider-authorized.
|
|
112
|
-
- Remote does not expose a direct shell, PTY, general tool RPC, remote desktop, or direct file-mutation API. Harness tools may still modify files or run commands under the Host's normal permission controls.
|
|
113
|
-
- Data is stored under `$DSH_HOME/remote/servers/<origin-hash>/{host,client}`. Unix private keys must be `0600`.
|
|
53
|
+
`/remote login` uses Zhihu QR authorization by default; GitHub QR authorization is also supported.
|
|
114
54
|
|
|
115
|
-
|
|
116
|
-
- Host/Client 使用长期 X25519 key + Noise IK 进行相互认证。
|
|
117
|
-
- ApiProxy 方法与 Typert Remote endpoint 均使用固定白名单(未命中即拒绝)。
|
|
118
|
-
- Workspace 选择器目录浏览仅返回元数据;可选的 File Viewer 预览只读、分块且继续执行 provider 授权。
|
|
119
|
-
- Remote 不开放直接 Shell、PTY、通用工具 RPC、远程桌面或直接文件修改 API。Harness 工具仍可以在 Host 原有权限控制下修改文件或运行命令。
|
|
120
|
-
- 数据位于 `$DSH_HOME/remote/servers/<origin-hash>/{host,client}`,私钥权限需 `0600`。
|
|
55
|
+
## Quick start
|
|
121
56
|
|
|
122
|
-
|
|
57
|
+
1. Open **Remote** from the Harness sidebar.
|
|
58
|
+
2. Sign in with GitHub or Zhihu QR authorization, or with your account and password.
|
|
59
|
+
3. Enable remote control for the computer that will remain running.
|
|
60
|
+
4. Sign in to the same account from DSH Desktop, [Remote Web](https://dsh.r2049.cn/app), or the [Android client](https://github.com/liguobao/ds-harness-remote/releases/latest).
|
|
61
|
+
5. Select the online Host and open one of its workspaces.
|
|
123
62
|
|
|
124
|
-
|
|
63
|
+
The public service currently uses the hosted Remote relay at `dsh.r2049.cn`. A supported self-hosted relay option is not available yet.
|
|
125
64
|
|
|
126
|
-
|
|
65
|
+
## Experimental Codex workspaces
|
|
127
66
|
|
|
128
|
-
|
|
67
|
+
Codex Remote is an optional domain inside this plugin. It lets authorized clients open projects reported by the Codex App Server while continuing to use the existing Harness or Android workspace and conversation UI.
|
|
129
68
|
|
|
130
|
-
|
|
131
|
-
[dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI) profile, install the plugin, start `dsh-tui`, and
|
|
132
|
-
use the native `/remote` slash command:
|
|
69
|
+
Codex support is enabled by default and can be disabled in the DeepSeek Remote settings card. It remains experimental while long-running recovery and cross-version compatibility work continue. See the [Codex Remote technical notes](https://github.com/liguobao/ds-harness-remote/blob/main/docs/codex-remote.md) for the current boundaries and validation status.
|
|
133
70
|
|
|
134
|
-
|
|
135
|
-
`connection` 服务。安装插件并启动 `dsh-tui` 后,使用原生 `/remote` Slash Command:
|
|
71
|
+
## Security boundary
|
|
136
72
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
73
|
+
- Session traffic is encrypted on the Client and decrypted only by the selected Host using `Noise_IK_25519_ChaChaPoly_SHA256`.
|
|
74
|
+
- Account membership and the Host's locally pinned device identity must both authorize a connection.
|
|
75
|
+
- The Host creates outbound connections only; it does not listen on a public port.
|
|
76
|
+
- Remote does not expose a direct shell, PTY, general tool RPC, remote desktop, or file-mutation API. Harness tools continue to operate under the Host's normal permission controls.
|
|
77
|
+
- Workspace browsing returns bounded, read-only directory metadata. Optional file previews remain read-only and provider-authorized.
|
|
78
|
+
- The relay can observe necessary connection metadata but cannot read session messages, prompts, tool output, workspace paths, or file-preview content.
|
|
140
79
|
|
|
141
|
-
|
|
142
|
-
/remote
|
|
143
|
-
/remote login [github|zhihu]
|
|
144
|
-
/remote status
|
|
145
|
-
/remote logout
|
|
146
|
-
```
|
|
80
|
+
## Compatibility
|
|
147
81
|
|
|
148
|
-
|
|
149
|
-
below the code. GitHub is also supported. dsh-TUI enables Host control by default; `/remote logout`
|
|
150
|
-
revokes the Host and rotates its local identity. Host configuration is intentionally not exposed yet;
|
|
151
|
-
the Server is `https://dsh.r2049.cn`.
|
|
152
|
-
|
|
153
|
-
`/remote login` 默认使用知乎,并打开 TUI 原生二维码场景,二维码下方会显示可点击的授权 URL;
|
|
154
|
-
同时支持 GitHub。dsh-TUI 默认开启 Host 控制;`/remote logout` 会撤销 Host 并轮换本地身份。
|
|
155
|
-
目前不提供 Host 配置,Server 固定为 `https://dsh.r2049.cn`。
|
|
156
|
-
|
|
157
|
-
Install the package through `dsh plugin`; installing it directly with npm does not update a DSH
|
|
158
|
-
profile or add the bundle configuration layer. Use `/remote` for dsh-TUI Host authorization and
|
|
159
|
-
status management. Server presence remains visible to authorized Remote clients while dsh-TUI is
|
|
160
|
-
running.
|
|
161
|
-
|
|
162
|
-
请通过 `dsh plugin` 安装。直接使用 npm 安装不会更新 DSH profile,也不会加入 bundle 配置层。
|
|
163
|
-
dsh-TUI Host 的授权与状态管理统一使用 `/remote`;dsh-TUI 运行时,Server presence 仍由已授权的
|
|
164
|
-
Remote Client 查询。
|
|
165
|
-
|
|
166
|
-
Codex Remote is an experimental optional domain inside this same plugin. It is enabled by default,
|
|
167
|
-
can be disabled from the DeepSeek Remote settings card, and lets authorized clients open Host Codex
|
|
168
|
-
projects in the existing Remote workspace and session UI. Desktop and Android can also browse to an
|
|
169
|
-
existing Host directory, register it as a Codex project, and open the resulting virtual workspace. See
|
|
170
|
-
[Codex Remote technical notes](../../docs/codex-remote.md) for workspace authority, data boundaries,
|
|
171
|
-
configuration, and validation status.
|
|
172
|
-
|
|
173
|
-
Codex Remote 是当前插件内部的实验性可选领域,默认开启,可在 DeepSeek Remote 设置卡片中关闭。它让
|
|
174
|
-
已授权客户端可以在现有 Remote Workspace 和 Session UI 中打开 Host 上的 Codex 项目。Desktop 与
|
|
175
|
-
Android 还可浏览并选择 Host 上已存在的目录,将它注册为 Codex Project 后直接打开对应虚拟 Workspace。
|
|
176
|
-
Workspace 来源、数据边界、配置方式和验证状态见
|
|
177
|
-
[Codex Remote 技术说明](../../docs/codex-remote.md)。
|
|
82
|
+
Plugin `0.4.9` supports:
|
|
178
83
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
```
|
|
84
|
+
- DeepSeek Harness `dsh-v0.1.1-rc.2` through the official legacy `ApiProxy`
|
|
85
|
+
- DeepSeek Harness `dsh-v0.1.2-alpha.1` through `dsh-v0.1.2-rc.1` through the official Typert Remote Gateway
|
|
182
86
|
|
|
183
|
-
|
|
87
|
+
Both Desktop endpoints must use the same Harness transport generation. ApiProxy and Typert connections are not translated and mixed-generation connections are rejected.
|
|
184
88
|
|
|
185
|
-
|
|
89
|
+
## Documentation
|
|
186
90
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
不要直接运行 npm;它不会配置 DSH profile。
|
|
91
|
+
- [Complete English guide](https://github.com/liguobao/ds-harness-remote#readme)
|
|
92
|
+
- [完整中文说明](https://github.com/liguobao/ds-harness-remote/blob/main/README.zh.md)
|
|
93
|
+
- [dsh-TUI Remote guide](https://github.com/liguobao/ds-harness-remote/blob/main/docs/dsh-tui.md)
|
|
94
|
+
- [End-to-end encryption](https://github.com/liguobao/ds-harness-remote/blob/main/docs/end-to-end-encryption.md)
|
|
95
|
+
- [Network and transport](https://github.com/liguobao/ds-harness-remote/blob/main/docs/network.md)
|
|
96
|
+
- [Protocol reference](https://github.com/liguobao/ds-harness-remote/blob/main/docs/protocol.md)
|
|
194
97
|
|
|
195
|
-
|
|
98
|
+
## Project status
|
|
196
99
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
```text
|
|
200
|
-
github:liguobao/ds-harness-remote#v0.4.7
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
```sh
|
|
204
|
-
dsh plugin --profile web add "github:liguobao/ds-harness-remote#v0.4.7"
|
|
205
|
-
```
|
|
100
|
+
This is an independent community project and is not an official DeepSeek product. DeepSeek and related names and marks belong to their respective owners.
|
|
206
101
|
|
|
207
|
-
|
|
102
|
+
Licensed under the [MIT License](https://github.com/liguobao/ds-harness-remote/blob/main/packages/plugin/LICENSE).
|
package/dist/client-runtime.d.ts
CHANGED
|
@@ -93,6 +93,8 @@ export declare class ClientModeRuntime {
|
|
|
93
93
|
private readonly proxySwitch?;
|
|
94
94
|
private readonly gatewaySwitch;
|
|
95
95
|
private readonly codexStreams;
|
|
96
|
+
private connectionProgress?;
|
|
97
|
+
private connectionProgressRun;
|
|
96
98
|
private closed;
|
|
97
99
|
constructor(config: ResolvedConfig, identities: IdentityStore, server: ClientServerApi, apiProxy: ApiProxy | undefined, typertGateway: TypertGatewayLike, logger: SafeLogger, host?: HostAuthorizationControl | undefined, rtcFactoryProvider?: (options?: WeriftFactoryOptions) => Promise<RtcPeerConnectionFactory | undefined>);
|
|
98
100
|
start(): Promise<void>;
|
|
@@ -131,6 +133,8 @@ export declare class ClientModeRuntime {
|
|
|
131
133
|
private assertRemoteCompatible;
|
|
132
134
|
private connect;
|
|
133
135
|
private ensureConnected;
|
|
136
|
+
private updateConnectionProgress;
|
|
137
|
+
private clearConnectionProgress;
|
|
134
138
|
handleControl(endpoint: string, payload: unknown, signal: AbortSignal): Promise<RpcResult<unknown>>;
|
|
135
139
|
private requireIdentity;
|
|
136
140
|
private authorizeHostPeer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-runtime.d.ts","sourceRoot":"","sources":["../src/client-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAA;AAC7E,OAAO,KAAK,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AACvF,OAAO,EAAqB,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC7E,OAAO,EAKL,KAAK,wBAAwB,EAC9B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAExE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,OAAO,KAAK,EAAgB,aAAa,EAAe,MAAM,qBAAqB,CAAA;AACnF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAErE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAG9C,OAAO,EAIL,KAAK,yBAAyB,EAC/B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACL,eAAe,EAGf,KAAK,aAAa,EAEnB,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"client-runtime.d.ts","sourceRoot":"","sources":["../src/client-runtime.ts"],"names":[],"mappings":"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"}
|
package/dist/client-runtime.js
CHANGED
|
@@ -27,6 +27,8 @@ export class ClientModeRuntime {
|
|
|
27
27
|
proxySwitch;
|
|
28
28
|
gatewaySwitch;
|
|
29
29
|
codexStreams = new Map();
|
|
30
|
+
connectionProgress;
|
|
31
|
+
connectionProgressRun = 0;
|
|
30
32
|
closed = false;
|
|
31
33
|
constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory) {
|
|
32
34
|
this.config = config;
|
|
@@ -68,7 +70,17 @@ export class ClientModeRuntime {
|
|
|
68
70
|
...targetStatus,
|
|
69
71
|
connected: this.connected !== undefined,
|
|
70
72
|
transport: this.connected?.client.getStats().mode ?? 'Disconnected',
|
|
73
|
+
connectedTargetDeviceId: this.connected?.target.deviceId,
|
|
71
74
|
preferredTransports: this.config.forceRelay ? ['relay'] : ['lan', 'p2p', 'turn', 'relay'],
|
|
75
|
+
...(this.connectionProgress === undefined ? {} : {
|
|
76
|
+
connectionProgress: {
|
|
77
|
+
targetDeviceId: this.connectionProgress.targetDeviceId,
|
|
78
|
+
phase: this.connectionProgress.phase,
|
|
79
|
+
...(this.connectionProgress.activeTransports === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { activeTransports: [...this.connectionProgress.activeTransports] }),
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
72
84
|
remoteFeatures: this.connected?.features ?? remoteHostFeatures(),
|
|
73
85
|
...(this.pendingWorkspaceSelection === undefined
|
|
74
86
|
? {}
|
|
@@ -144,6 +156,7 @@ export class ClientModeRuntime {
|
|
|
144
156
|
async clearClientAuthorization() {
|
|
145
157
|
const previous = this.connected;
|
|
146
158
|
this.connected = undefined;
|
|
159
|
+
this.connectionProgress = undefined;
|
|
147
160
|
this.pendingWorkspaceSelection = undefined;
|
|
148
161
|
await this.closeCodexVirtual();
|
|
149
162
|
this.proxySwitch?.selectLocal();
|
|
@@ -172,6 +185,7 @@ export class ClientModeRuntime {
|
|
|
172
185
|
this.gatewaySwitch.selectLocal();
|
|
173
186
|
const previous = this.connected;
|
|
174
187
|
this.connected = undefined;
|
|
188
|
+
this.connectionProgress = undefined;
|
|
175
189
|
this.pendingWorkspaceSelection = undefined;
|
|
176
190
|
await this.closeCodexStreams(previous?.client);
|
|
177
191
|
await previous?.client.close().catch(() => undefined);
|
|
@@ -186,11 +200,13 @@ export class ClientModeRuntime {
|
|
|
186
200
|
this.assertRemoteCompatible(next);
|
|
187
201
|
}
|
|
188
202
|
catch (error) {
|
|
203
|
+
this.clearConnectionProgress(next.progressRunId);
|
|
189
204
|
await next.client.close().catch(() => undefined);
|
|
190
205
|
throw error;
|
|
191
206
|
}
|
|
192
207
|
const previous = this.connected;
|
|
193
208
|
this.connected = next;
|
|
209
|
+
this.clearConnectionProgress(next.progressRunId);
|
|
194
210
|
this.pendingWorkspaceSelection = undefined;
|
|
195
211
|
await this.closeCodexVirtual();
|
|
196
212
|
this.selectRemoteTarget(next);
|
|
@@ -340,6 +356,7 @@ export class ClientModeRuntime {
|
|
|
340
356
|
await this.closeCodexStreams(this.connected?.client);
|
|
341
357
|
await this.connected?.client.close().catch(() => undefined);
|
|
342
358
|
this.connected = undefined;
|
|
359
|
+
this.connectionProgress = undefined;
|
|
343
360
|
this.proxySwitch?.restore();
|
|
344
361
|
this.gatewaySwitch.restore();
|
|
345
362
|
}
|
|
@@ -541,61 +558,66 @@ export class ClientModeRuntime {
|
|
|
541
558
|
}
|
|
542
559
|
async connect(targetDeviceId, signal) {
|
|
543
560
|
signal?.throwIfAborted();
|
|
561
|
+
const progressRunId = this.connectionProgressRun + 1;
|
|
562
|
+
this.connectionProgressRun = progressRunId;
|
|
563
|
+
this.connectionProgress = { runId: progressRunId, targetDeviceId, phase: 'checking-host' };
|
|
544
564
|
const identity = this.requireIdentity();
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
let webRtcFallback = false;
|
|
563
|
-
const createTransport = (attempt) => new AdaptiveTransport(websocketUrl(this.server.baseUrl), {
|
|
564
|
-
role: 'client',
|
|
565
|
-
deviceId: identity.deviceId,
|
|
566
|
-
accessToken: credentials.accessToken,
|
|
567
|
-
targetDeviceId,
|
|
568
|
-
forceRelay: this.config.forceRelay || attempt === 'relay',
|
|
569
|
-
preferredTransports: preferredTransportsForAttempt(attempt),
|
|
570
|
-
negotiateTimeoutMs: attempt === 'direct' ? DIRECT_WEBRTC_NEGOTIATE_TIMEOUT_MS : undefined,
|
|
571
|
-
...(rtcFactory === undefined || attempt === 'relay' ? {} : { rtcFactory }),
|
|
572
|
-
fetchIceServers: async (connectionId) => iceServersForAttempt(attempt, await this.server.turnCredentials(connectionId)),
|
|
573
|
-
onWebRtcFallback: (error, diagnostics) => {
|
|
574
|
-
webRtcFallback = true;
|
|
575
|
-
this.logger.warn(attempt === 'direct'
|
|
576
|
-
? 'remote Harness direct WebRTC failed; trying TURN'
|
|
577
|
-
: 'remote Harness TURN WebRTC failed; using relay', {
|
|
565
|
+
let client;
|
|
566
|
+
try {
|
|
567
|
+
const serverDevice = (await this.server.listDevices()).find(device => device.deviceId === targetDeviceId);
|
|
568
|
+
if (serverDevice === undefined) {
|
|
569
|
+
throw new ClientModeError('MEMBERSHIP_REQUIRED', 'The selected Host is not authorized for this account.');
|
|
570
|
+
}
|
|
571
|
+
this.updateConnectionProgress(progressRunId, 'authorizing-peer');
|
|
572
|
+
const target = await this.authorizeHostPeer(serverDevice);
|
|
573
|
+
const presence = await this.server.presenceFor(targetDeviceId);
|
|
574
|
+
if (!presence.online)
|
|
575
|
+
throw new ClientModeError('HOST_OFFLINE', 'The selected Host is offline.', true);
|
|
576
|
+
const credentials = await this.server.authenticate(identity);
|
|
577
|
+
const rtcFactory = this.config.forceRelay
|
|
578
|
+
? undefined
|
|
579
|
+
: await this.rtcFactoryProvider({ routeTargets: [this.server.baseUrl] }).catch(() => undefined);
|
|
580
|
+
if (!this.config.forceRelay && rtcFactory === undefined) {
|
|
581
|
+
this.logger.warn('remote Harness WebRTC backend unavailable; using relay', {
|
|
578
582
|
targetDeviceId: shortId(target.deviceId),
|
|
579
|
-
attempt,
|
|
580
|
-
reason: diagnosticReason(error),
|
|
581
583
|
});
|
|
582
|
-
|
|
583
|
-
|
|
584
|
+
}
|
|
585
|
+
let webRtcFallback = false;
|
|
586
|
+
const createTransport = (attempt) => new AdaptiveTransport(websocketUrl(this.server.baseUrl), {
|
|
587
|
+
role: 'client',
|
|
588
|
+
deviceId: identity.deviceId,
|
|
589
|
+
accessToken: credentials.accessToken,
|
|
590
|
+
targetDeviceId,
|
|
591
|
+
forceRelay: this.config.forceRelay || attempt === 'relay',
|
|
592
|
+
preferredTransports: preferredTransportsForAttempt(attempt),
|
|
593
|
+
negotiateTimeoutMs: attempt === 'direct' ? DIRECT_WEBRTC_NEGOTIATE_TIMEOUT_MS : undefined,
|
|
594
|
+
...(rtcFactory === undefined || attempt === 'relay' ? {} : { rtcFactory }),
|
|
595
|
+
fetchIceServers: async (connectionId) => iceServersForAttempt(attempt, await this.server.turnCredentials(connectionId)),
|
|
596
|
+
onWebRtcFallback: (error, diagnostics) => {
|
|
597
|
+
webRtcFallback = true;
|
|
598
|
+
this.logger.warn(attempt === 'direct'
|
|
599
|
+
? 'remote Harness direct WebRTC failed; trying TURN'
|
|
600
|
+
: 'remote Harness TURN WebRTC failed; using relay', {
|
|
584
601
|
targetDeviceId: shortId(target.deviceId),
|
|
585
602
|
attempt,
|
|
586
|
-
|
|
603
|
+
reason: diagnosticReason(error),
|
|
587
604
|
});
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
605
|
+
if (diagnostics !== undefined) {
|
|
606
|
+
this.logger.debug('remote Harness WebRTC fallback diagnostics', {
|
|
607
|
+
targetDeviceId: shortId(target.deviceId),
|
|
608
|
+
attempt,
|
|
609
|
+
...webrtcDiagnosticsLogFields(diagnostics),
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
});
|
|
614
|
+
const attempts = this.config.forceRelay || rtcFactory === undefined
|
|
615
|
+
? ['relay']
|
|
616
|
+
: ['direct', 'turn', 'relay'];
|
|
617
|
+
let transport;
|
|
597
618
|
for (const attempt of attempts) {
|
|
598
619
|
webRtcFallback = false;
|
|
620
|
+
this.updateConnectionProgress(progressRunId, 'probing', activeTransportsForAttempt(attempt));
|
|
599
621
|
transport = createTransport(attempt);
|
|
600
622
|
client = new RemoteClientCore(new ClientSecureTransport(transport, identity, target), 60_000);
|
|
601
623
|
await client.connect();
|
|
@@ -616,10 +638,13 @@ export class ClientModeRuntime {
|
|
|
616
638
|
}
|
|
617
639
|
const connectedClient = client;
|
|
618
640
|
const connectedTransport = transport;
|
|
641
|
+
const connectedPreference = transportPreferenceForMode(connectedClient.getStats().mode);
|
|
642
|
+
this.updateConnectionProgress(progressRunId, 'connected', connectedPreference === undefined ? undefined : [connectedPreference]);
|
|
619
643
|
connectedClient.onClose(() => {
|
|
620
644
|
if (this.connected?.client !== connectedClient)
|
|
621
645
|
return;
|
|
622
646
|
this.connected = undefined;
|
|
647
|
+
this.connectionProgress = undefined;
|
|
623
648
|
this.pendingWorkspaceSelection = undefined;
|
|
624
649
|
void this.closeCodexVirtual();
|
|
625
650
|
this.proxySwitch?.selectLocal();
|
|
@@ -651,10 +676,12 @@ export class ClientModeRuntime {
|
|
|
651
676
|
target,
|
|
652
677
|
transport: connectedTransport,
|
|
653
678
|
features,
|
|
679
|
+
progressRunId,
|
|
654
680
|
...(serverDevice.clientVersion === undefined ? {} : { clientVersion: serverDevice.clientVersion }),
|
|
655
681
|
};
|
|
656
682
|
}
|
|
657
683
|
catch (error) {
|
|
684
|
+
this.clearConnectionProgress(progressRunId);
|
|
658
685
|
await client?.close().catch(() => undefined);
|
|
659
686
|
throw error;
|
|
660
687
|
}
|
|
@@ -665,9 +692,24 @@ export class ClientModeRuntime {
|
|
|
665
692
|
const next = await this.connect(targetDeviceId, signal);
|
|
666
693
|
const previous = this.connected;
|
|
667
694
|
this.connected = next;
|
|
695
|
+
this.clearConnectionProgress(next.progressRunId);
|
|
668
696
|
await previous?.client.close().catch(() => undefined);
|
|
669
697
|
return next;
|
|
670
698
|
}
|
|
699
|
+
updateConnectionProgress(runId, phase, activeTransports) {
|
|
700
|
+
if (this.connectionProgress?.runId !== runId)
|
|
701
|
+
return;
|
|
702
|
+
this.connectionProgress = {
|
|
703
|
+
runId,
|
|
704
|
+
targetDeviceId: this.connectionProgress.targetDeviceId,
|
|
705
|
+
phase,
|
|
706
|
+
...(activeTransports === undefined ? {} : { activeTransports }),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
clearConnectionProgress(runId) {
|
|
710
|
+
if (this.connectionProgress?.runId === runId)
|
|
711
|
+
this.connectionProgress = undefined;
|
|
712
|
+
}
|
|
671
713
|
async handleControl(endpoint, payload, signal) {
|
|
672
714
|
try {
|
|
673
715
|
if (endpoint === 'status')
|
|
@@ -1077,6 +1119,22 @@ function preferredTransportsForAttempt(attempt) {
|
|
|
1077
1119
|
return ['turn', 'relay'];
|
|
1078
1120
|
return ['relay'];
|
|
1079
1121
|
}
|
|
1122
|
+
function activeTransportsForAttempt(attempt) {
|
|
1123
|
+
if (attempt === 'direct')
|
|
1124
|
+
return ['lan', 'p2p'];
|
|
1125
|
+
return [attempt];
|
|
1126
|
+
}
|
|
1127
|
+
function transportPreferenceForMode(mode) {
|
|
1128
|
+
if (mode === 'LAN')
|
|
1129
|
+
return 'lan';
|
|
1130
|
+
if (mode === 'P2P')
|
|
1131
|
+
return 'p2p';
|
|
1132
|
+
if (mode === 'TURN')
|
|
1133
|
+
return 'turn';
|
|
1134
|
+
if (mode === 'Relay')
|
|
1135
|
+
return 'relay';
|
|
1136
|
+
return undefined;
|
|
1137
|
+
}
|
|
1080
1138
|
function iceServersForAttempt(attempt, iceServers) {
|
|
1081
1139
|
return attempt === 'direct' ? stunOnlyIceServers(iceServers) : iceServers;
|
|
1082
1140
|
}
|