dsh-computers 1.1.0 → 1.3.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/PROTOCOL.md +135 -0
- package/README.md +45 -16
- package/agent/README.md +112 -5
- package/agent/dsh-computers-agent.mjs +835 -20
- package/client.js +777 -10
- package/cordis.patch.yml +2 -0
- package/index.js +32 -2
- package/lib/access.js +397 -0
- package/lib/fs-ops.js +162 -0
- package/lib/hub.js +346 -140
- package/lib/routes.js +425 -38
- package/lib/state.js +11 -2
- package/lib/tools.js +684 -85
- package/lib/version.js +62 -0
- package/package.json +1 -1
package/PROTOCOL.md
CHANGED
|
@@ -557,3 +557,138 @@ the raw materials for the desktop path: below the command box, a muted line
|
|
|
557
557
|
「也可以在这台电脑的 DSH 桌面端打开 设置 → 电脑 → 本机运行时,填入下面的地址和密钥」 plus
|
|
558
558
|
two compact copyable rows — the server URL and the enrollment key (`/keys` already returns
|
|
559
559
|
`serverUrl` and `key`). That is how a user connects a machine without a terminal.
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## 11. Stage 1 (v1.3.0): capabilities, streaming, remote files
|
|
564
|
+
|
|
565
|
+
Everything here is additive: an agent that reports no capabilities keeps the v1 behaviour
|
|
566
|
+
exactly. The wire stays SSE (host → agent) + HTTP POST (agent → host); no inbound ports.
|
|
567
|
+
|
|
568
|
+
### 11.1 Capabilities and version gate
|
|
569
|
+
|
|
570
|
+
`POST /agent/enroll` and `POST /agent/heartbeat` carry
|
|
571
|
+
`"capabilities": ["exec" | "fs" | "pty"]` and `agentVersion` (already present). The registry
|
|
572
|
+
stores the last reported set and `capabilitiesUpdatedAt`. Rules:
|
|
573
|
+
|
|
574
|
+
- A computer with no reported capabilities is treated as `["exec"]` (v1 agents).
|
|
575
|
+
- Host tool availability is gated per session: the `computer_*_file` tools are only
|
|
576
|
+
registered/offered when at least one allowed computer reports `fs`; the terminal backend
|
|
577
|
+
only serves computers reporting `pty`. Gating uses `ctx.tools.restrict`-style filtering
|
|
578
|
+
(drop the schema) rather than failing at call time, so the model never sees a tool it
|
|
579
|
+
cannot use; a call that still arrives for an incapable computer answers with an
|
|
580
|
+
actionable "this machine's agent does not support file access (upgrade the agent)".
|
|
581
|
+
- `hello` echoes the host's view: `{ computerId, name, heartbeatMs, capabilities }`.
|
|
582
|
+
- `/state` and `/health` expose each computer's `agentVersion` and `capabilities`; the
|
|
583
|
+
settings page shows a drift badge when `agentVersion` differs from the host plugin's.
|
|
584
|
+
|
|
585
|
+
### 11.2 Streaming results
|
|
586
|
+
|
|
587
|
+
`exec` gains `"stream": true`. The agent then POSTs incremental output to
|
|
588
|
+
`POST /api/dsh-computers/agent/results/chunk`:
|
|
589
|
+
|
|
590
|
+
```json
|
|
591
|
+
{ "requestId": "req_…", "seq": 0, "stdout": "<base64>", "stderr": "<base64>" }
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
- `seq` starts at 0 and increases by 1; the host drops out-of-order duplicates and
|
|
595
|
+
reassembles. Batches are sent at most every 100 ms (or 32 KiB, whichever first).
|
|
596
|
+
- The final `POST /agent/results` stays authoritative for `exitCode`/`durationMs`/
|
|
597
|
+
`timedOut`/`truncated` and carries the same capped buffers as v1 (so a host that missed
|
|
598
|
+
chunks still ends with the tail).
|
|
599
|
+
- Host side, a streaming execution feeds the **DSH jobs** registry: `computer_exec` with
|
|
600
|
+
`run_in_background: true` registers one job (`kind: 'computer-exec'`, label
|
|
601
|
+
`<name>: <command>`, owner = calling session), returns its id immediately, and appends
|
|
602
|
+
every chunk to the job's output ring so the built-in `job_output` / `job_kill` tools work
|
|
603
|
+
unchanged. Killing the job sends `cancel` to the agent and settles the job.
|
|
604
|
+
- A foreground call still returns the complete result (v1 shape) but the host keeps the
|
|
605
|
+
tail in the job ring for the conversation's progress view.
|
|
606
|
+
|
|
607
|
+
### 11.3 Remote file access
|
|
608
|
+
|
|
609
|
+
Host → agent, over the same SSE stream:
|
|
610
|
+
|
|
611
|
+
```json
|
|
612
|
+
{ "event": "fs", "data": { "requestId": "fsr_…", "op": "read", "path": "/abs", "encoding": "utf8", "offset": 0, "limit": 2000 } }
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
`op` ∈ `read | write | list | stat | mkdir | rename | delete | glob | grep`.
|
|
616
|
+
Agent → host: `POST /api/dsh-computers/agent/fs/result`
|
|
617
|
+
|
|
618
|
+
```json
|
|
619
|
+
{ "requestId": "fsr_…", "ok": true, "op": "read", "path": "/abs",
|
|
620
|
+
"content": "<base64 for write/read binary>", "text": "<utf8 for text reads>",
|
|
621
|
+
"encoding": "utf8|base64", "bytes": 123, "lines": 20, "truncated": false,
|
|
622
|
+
"entries": [{ "name": "a", "path": "/abs/a", "type": "file|dir|symlink", "size": 1, "mtimeMs": 0 }],
|
|
623
|
+
"stat": { "type": "file", "size": 1, "mtimeMs": 0, "mode": 420 },
|
|
624
|
+
"matches": [{ "path": "/abs/a", "line": 3, "text": "…" }],
|
|
625
|
+
"error": null }
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Limits: read 1 MiB / 20 000 lines, write 4 MiB, list 5 000 entries, glob 2 000 paths,
|
|
629
|
+
grep 500 matches, per-request timeout 15 s. Paths must be absolute; the agent rejects a
|
|
630
|
+
relative path with `ok:false, error:"path must be absolute"`.
|
|
631
|
+
|
|
632
|
+
Host tools (registered only when a `fs`-capable computer is available, §11.1):
|
|
633
|
+
|
|
634
|
+
| tool | arguments (JSON Schema) | value |
|
|
635
|
+
|---|---|---|
|
|
636
|
+
| `computer_read` | `computer?`, `path`, `offset?`, `limit?` | `{ computer, path, text, lines, bytes, truncated }` |
|
|
637
|
+
| `computer_write` | `computer?`, `path`, `content`, `createDirs?` | `{ computer, path, bytes, created }` |
|
|
638
|
+
| `computer_edit` | `computer?`, `path`, `old`, `new`, `replaceAll?` | `{ computer, path, replacements, bytes }` |
|
|
639
|
+
| `computer_glob` | `computer?`, `pattern`, `cwd?` | `{ computer, paths }` |
|
|
640
|
+
| `computer_grep` | `computer?`, `pattern`, `path?`, `include?`, `maxMatches?` | `{ computer, matches }` |
|
|
641
|
+
|
|
642
|
+
`computer` is optional in all of them and falls back to the session pin (§10/§9 selection),
|
|
643
|
+
same as `computer_exec`. Renders mirror the built-in `read`/`write`/`edit`/`glob`/`grep`
|
|
644
|
+
tool output style so the model reads them the same way. A write/edit to a path the agent
|
|
645
|
+
cannot touch answers `ok:false` with the OS error text, surfaced as a tool error.
|
|
646
|
+
|
|
647
|
+
### 11.4 PTY (terminal backend) — follows in v1.4.0
|
|
648
|
+
|
|
649
|
+
Reserved events, not implemented in v1.3.0:
|
|
650
|
+
`{"event":"pty.open"|"pty.input"|"pty.resize"|"pty.close", data:{ ptyId, … }}` and
|
|
651
|
+
`POST /agent/pty/out` `{ ptyId, seq, data(base64) }` + `POST /agent/pty/exit` `{ ptyId, exitCode }`.
|
|
652
|
+
The host will register a DSH `terminals` backend (`ctx.terminals.registerBackend`) so the
|
|
653
|
+
built-in terminal tools and UI work against a connected computer; agents advertise `pty`
|
|
654
|
+
only once they implement it.
|
|
655
|
+
|
|
656
|
+
### 11.5 Compatibility
|
|
657
|
+
|
|
658
|
+
- v1 agents: no capabilities, no streaming, no fs — everything above degrades to §3/§4.
|
|
659
|
+
- New host + old agent: `computer_exec` unchanged; file tools hidden; a manual call errors
|
|
660
|
+
with the upgrade hint.
|
|
661
|
+
- New agent + old host: the agent never sends `stream:true` payloads unless the host's
|
|
662
|
+
`hello` advertises `"features":["stream","fs","pty"]`, so an old host is unaffected.
|
|
663
|
+
|
|
664
|
+
### 11.6 Interaction details aligned with the reference product
|
|
665
|
+
|
|
666
|
+
These mirror the reference implementation's runtime protocol *behaviour* (handshake metadata,
|
|
667
|
+
stream frames, background adoption, update-required) without copying any of its code.
|
|
668
|
+
|
|
669
|
+
1. **Handshake metadata.** The agent's enroll body and every reconnect carry a metadata block:
|
|
670
|
+
`{ version: 1, agentVersion, hostname, os, arch, capabilities, workspaceBase }`, where
|
|
671
|
+
`workspaceBase` is the agent's default working directory (config `workspaceBase`, default
|
|
672
|
+
`os.homedir()`), reported back to the host at connect and used as the default `cwd` for
|
|
673
|
+
`exec` and the base for relative `glob`/`grep`. The host stores it and shows it in the
|
|
674
|
+
computer detail view. Same shape as the reference product's `RuntimeHandshakeMetadataV1`.
|
|
675
|
+
2. **Stream frames.** Chunk payloads carry a `type`:
|
|
676
|
+
`{ type: 'stdout' | 'stderr' | 'heartbeat', data(base64, empty for heartbeat) }`.
|
|
677
|
+
The agent emits a `heartbeat` frame for every 10 s of a silent command so the host can keep
|
|
678
|
+
the execution alive and the conversation's progress view ticking instead of timing out.
|
|
679
|
+
Exit code still arrives exactly once in the final `/agent/results`.
|
|
680
|
+
3. **Timeout adopts instead of killing.** When a foreground `computer_exec` reaches
|
|
681
|
+
`timeoutMs`, the host does NOT kill it by default: it moves the still-running execution
|
|
682
|
+
into the background-job path (config `promoteOnTimeout`, default true), returns `{ jobId,
|
|
683
|
+
promoted: true }`, and the model continues with `job_output`/`job_kill`. With
|
|
684
|
+
`promoteOnTimeout: false` the v1 cancel-and-error behaviour applies.
|
|
685
|
+
4. **Pending → activated lifecycle.** An enrollment key is `pending` until the agent's first
|
|
686
|
+
successful stream; that moment sets `activatedAt` and marks the key used. A never-activated
|
|
687
|
+
computer is hidden from the list (only `pendingKey` shows in the connect dialog), matching
|
|
688
|
+
the reference product's activation lifecycle.
|
|
689
|
+
5. **Update-required.** Config `minAgentVersion` (default: the plugin's major.minor) gates
|
|
690
|
+
tool calls: an older agent gets an actionable "upgrade this computer's agent" error, and
|
|
691
|
+
`/state` marks the computer `drift: true` with `updateRequired: true`.
|
|
692
|
+
6. **Revoke kicks the stream.** Deleting a computer ends its live SSE stream in the same
|
|
693
|
+
request and invalidates the token, so a stale agent gets `401 invalid-token` and stops
|
|
694
|
+
retrying until re-enrolled.
|
package/README.md
CHANGED
|
@@ -26,25 +26,36 @@
|
|
|
26
26
|
|
|
27
27
|
插件是纯 JavaScript,无构建、无第三方依赖,直接从 GitHub 装:
|
|
28
28
|
|
|
29
|
+
**推荐从 npm 安装**(和官方插件包一样):
|
|
30
|
+
|
|
29
31
|
```bash
|
|
30
|
-
# 方式 A:CLI
|
|
31
|
-
dsh plugin install
|
|
32
|
+
# 方式 A:CLI
|
|
33
|
+
dsh plugin install dsh-computers
|
|
34
|
+
|
|
35
|
+
# 方式 B:DSH 设置 → 插件市场 → 安装,搜 dsh-computers
|
|
36
|
+
```
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
也可以直接从 GitHub 装(跟随仓库最新提交):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
dsh plugin install github:9Ashwin/dsh-computers
|
|
35
42
|
```
|
|
36
43
|
|
|
37
44
|
### 更新
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
**npm 安装**:插件市场 → 已安装 → 「检查更新」即可,或者在 CLI 里装具体版本:
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
```bash
|
|
49
|
+
dsh plugin install dsh-computers@1.3.0
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**GitHub 安装**:profile 里是按 spec + commit 固定的(`github:user/repo` 或 `#v1.0.2`),所以
|
|
53
|
+
|
|
54
|
+
- 用**同一个 spec** 再装一次只会得到 `already-installed`(这就是「再安装没法更新」的原因);
|
|
55
|
+
- 装 **tag** 会被冻住,市场永远检测不到更新;装**分支**(不带 `#`)才能被市场「检查更新」发现新提交;
|
|
56
|
+
- 手动升级到某个版本要**换 spec**:`dsh plugin install github:9Ashwin/dsh-computers#v1.3.0`。
|
|
57
|
+
|
|
58
|
+
> 更新完记得重启 Host 并刷新页面,具体见 [docs/deploy.md](./docs/deploy.md)。
|
|
48
59
|
|
|
49
60
|
装好后重启 `dsh web`,设置里会出现 **电脑** 页。
|
|
50
61
|
|
|
@@ -97,20 +108,38 @@ tail -f ~/.dsh-computers/agent.log
|
|
|
97
108
|
|
|
98
109
|
## 3. 对话里怎么用
|
|
99
110
|
|
|
100
|
-
|
|
111
|
+
智能体会看到这些工具(`computer` 参数都可省略,省略时用输入框左侧选中的目标电脑):
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
| 工具 | 作用 |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `computer_list` | 列出可用电脑(名字、平台、在线、能力、当前固定目标) |
|
|
116
|
+
| `computer_exec` | 执行命令,支持 `run_in_background`;超时默认**转后台**而不是杀掉 |
|
|
117
|
+
| `computer_read` | 读远端文件(offset/limit 行) |
|
|
118
|
+
| `computer_write` | 写远端文件(会自动建目录) |
|
|
119
|
+
| `computer_edit` | 字面替换(`old` 必须唯一,除非 `replaceAll`) |
|
|
120
|
+
| `computer_glob` | 远端 glob(`*`、`?`、`**`、`{a,b}`) |
|
|
121
|
+
| `computer_grep` | 远端正则搜索(支持 `include`、命中上限) |
|
|
122
|
+
|
|
123
|
+
文件类工具需要那台电脑的 agent 上报 `fs` 能力(见下)。
|
|
104
124
|
|
|
105
125
|
系统提示里会自动附上当前电脑清单,所以直接说「看看两台 mac 的内存」它就会分别调用。
|
|
106
126
|
|
|
127
|
+
## 3.1 能力、流式与后台任务
|
|
128
|
+
|
|
129
|
+
- **能力协商**:agent 在握手时上报 `capabilities`(`exec` / `fs`,以后还有 `pty`)和 `workspaceBase`(默认工作目录)。服务器据此决定是否提供文件类工具;agent 过旧时工具调用会明确提示"升级这台电脑的 agent",设置页也会显示 `可升级`。
|
|
130
|
+
- **流式**:长命令的分块输出会随帧回传(含心跳),后台任务的输出可在 DSH 的任务里查看。
|
|
131
|
+
- **超时转后台**:前台命令超时默认不杀(`promoteOnTimeout: true`),而是转入后台任务并返回 jobId,之后用内置的 `job_output` / `job_kill` 接管。
|
|
132
|
+
- **激活时机**:新电脑要等 agent 首次建流后才出现在列表(从生成密钥到真正连上之间,弹窗会一直显示"等待电脑连接…")。
|
|
133
|
+
|
|
107
134
|
## 4. 端点一览
|
|
108
135
|
|
|
109
136
|
| 方法 | 路径 | 说明 |
|
|
110
137
|
|---|---|---|
|
|
111
138
|
| POST | `/api/dsh-computers/agent/enroll` | 加入密钥换取电脑 token(runtime 卡片也走这里) |
|
|
112
139
|
| GET | `/api/dsh-computers/agent/events` | 被控端的 SSE 长连接(Bearer token) |
|
|
113
|
-
| POST | `/api/dsh-computers/agent/results` |
|
|
140
|
+
| POST | `/api/dsh-computers/agent/results` | 命令执行结果回传(最终结果,权威) |
|
|
141
|
+
| POST | `/api/dsh-computers/agent/results/chunk` | 流式输出分块(stdout/stderr/heartbeat) |
|
|
142
|
+
| POST | `/api/dsh-computers/agent/fs/result` | 远端文件操作结果 |
|
|
114
143
|
| POST | `/api/dsh-computers/agent/heartbeat` | 心跳 |
|
|
115
144
|
| GET | `/api/dsh-computers/state` | 设置页数据(角色、电脑列表,需要浏览器会话) |
|
|
116
145
|
| POST | `/api/dsh-computers/keys` | 生成一次性加入密钥 |
|
package/agent/README.md
CHANGED
|
@@ -21,6 +21,13 @@ The same file is used two ways, with one implementation:
|
|
|
21
21
|
*本机运行时* card calls the module's exported `startRuntime()` in-process, so
|
|
22
22
|
the user never opens a terminal (PROTOCOL §10.4).
|
|
23
23
|
|
|
24
|
+
Since v1.3.0 the agent implements PROTOCOL §11: it advertises the `exec` and
|
|
25
|
+
`fs` capabilities, streams live output when the host asks for it, serves remote
|
|
26
|
+
file operations over the same SSE stream, and reports §11.6 handshake metadata
|
|
27
|
+
(`agentVersion`, `hostname`, `os`, `arch`, `capabilities`, `workspaceBase`) at
|
|
28
|
+
enroll and on every reconnect. Old hosts that advertise no `features` get
|
|
29
|
+
exactly the v1 behaviour.
|
|
30
|
+
|
|
24
31
|
The file is served verbatim by the host plugin at
|
|
25
32
|
`/api/dsh-computers/agent.mjs`, so what you run is exactly what the repository
|
|
26
33
|
contains. It imports only Node built-ins, never opens a listening socket, and
|
|
@@ -70,7 +77,7 @@ node ~/.dsh-computers/agent.mjs enroll --server http://<server> --key dshc_… -
|
|
|
70
77
|
## Commands
|
|
71
78
|
|
|
72
79
|
```
|
|
73
|
-
dsh-computers-agent enroll --server <url> --key <key> [--name <name>] [--replace]
|
|
80
|
+
dsh-computers-agent enroll --server <url> --key <key> [--name <name>] [--workspace-base <path>] [--replace]
|
|
74
81
|
dsh-computers-agent run
|
|
75
82
|
dsh-computers-agent service install|uninstall|start|stop|restart|status
|
|
76
83
|
dsh-computers-agent status
|
|
@@ -79,7 +86,7 @@ dsh-computers-agent --version | --help
|
|
|
79
86
|
|
|
80
87
|
| Command | What it does |
|
|
81
88
|
| --- | --- |
|
|
82
|
-
| `enroll` | POSTs the join key to `/api/dsh-computers/agent/enroll`, saves `{serverUrl, computerId, token, name, enrolledAt}` to `config.json` (atomic, `0600`), then opens the SSE stream for ~5 s to prove the round trip. Exits non-zero with the server's message on failure. |
|
|
89
|
+
| `enroll` | POSTs the join key (plus `capabilities` and §11.6 `metadata`) to `/api/dsh-computers/agent/enroll`, saves `{serverUrl, computerId, token, name, enrolledAt, workspaceBase}` to `config.json` (atomic, `0600`), then opens the SSE stream for ~5 s to prove the round trip. `--workspace-base` sets the default working directory (default `$HOME`). Exits non-zero with the server's message on failure. |
|
|
83
90
|
| `run` | Foreground worker: one SSE stream to `/api/dsh-computers/agent/events`; executes `exec` events, kills the process group on `cancel`, POSTs results, sends heartbeats, reconnects with jittered 1 s → 30 s backoff. |
|
|
84
91
|
| `status` | Prints server, computer id/name, redacted token (last 6 chars only), config/log paths and service state. Exit `0` when enrolled, `1` when not. |
|
|
85
92
|
| `service` | Login/auto-restart service. See below. |
|
|
@@ -155,11 +162,95 @@ Semantics guaranteed to the host:
|
|
|
155
162
|
|
|
156
163
|
`AGENT_VERSION` is exported too (the host can put it in the §3.1 enroll body).
|
|
157
164
|
|
|
165
|
+
## Protocol v1.3.0 / §11 features
|
|
166
|
+
|
|
167
|
+
### Capabilities and gating (§11.1, §11.5)
|
|
168
|
+
|
|
169
|
+
The agent reports `capabilities: ["exec", "fs"]` in the enroll body, in the
|
|
170
|
+
heartbeat `meta`, in the §11.6 metadata and in `startRuntime().status()`.
|
|
171
|
+
Nothing new is sent to an old host: the `hello` event's `features` list decides
|
|
172
|
+
what is enabled per connection —
|
|
173
|
+
|
|
174
|
+
| `hello.features` | behaviour |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| absent / no `stream`, no `fs` | exactly v1: `exec` results only, `fs` events ignored, no chunk POSTs |
|
|
177
|
+
| `stream` | `exec` with `"stream": true` gets incremental chunk POSTs |
|
|
178
|
+
| `fs` | `fs` events are served and answered on `/agent/fs/result` |
|
|
179
|
+
|
|
180
|
+
### Handshake metadata (§11.6)
|
|
181
|
+
|
|
182
|
+
Enroll sends `metadata: { version: 1, agentVersion, hostname, os, arch,
|
|
183
|
+
capabilities, workspaceBase }`, and every SSE connect (probe and each
|
|
184
|
+
reconnect) carries the same JSON in the `x-dsh-computers-metadata` header as
|
|
185
|
+
base64url — an SSE GET has no body, so the header is how the reference-product
|
|
186
|
+
handshake metadata maps onto this wire. `workspaceBase` comes from
|
|
187
|
+
`config.json` (`--workspace-base`, default `$HOME`), is the default `cwd` for
|
|
188
|
+
`exec`, and is the base for relative `glob`/`grep` patterns.
|
|
189
|
+
|
|
190
|
+
### Streaming results (§11.2 + §11.6 frames)
|
|
191
|
+
|
|
192
|
+
When the host advertises `stream` and an `exec` event has `stream: true`, the
|
|
193
|
+
agent POSTs frames to `/api/dsh-computers/agent/results/chunk`:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{ "requestId": "req_…", "seq": 0, "type": "stdout", "data": "<base64>" }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- `type` is `stdout`, `stderr`, or `heartbeat` (empty `data`).
|
|
200
|
+
- `seq` starts at 0 and increases by one per frame; frames are posted strictly
|
|
201
|
+
sequentially so arrival order matches `seq` (the host can still drop dupes).
|
|
202
|
+
- Batches flush at most every 100 ms or 32 KiB (whichever first); a heartbeat
|
|
203
|
+
frame is emitted after each 10 s of silence from a running command.
|
|
204
|
+
- A failed chunk POST retries once, then that batch is dropped — the command
|
|
205
|
+
keeps running and the final authoritative `POST /agent/results` still carries
|
|
206
|
+
the capped `stdout`/`stderr` buffers, `exitCode`, `durationMs`, `timedOut`
|
|
207
|
+
and `truncated`.
|
|
208
|
+
- `stop()` drops buffered frames, clears every chunk timer and leaves no pending
|
|
209
|
+
POSTs (they are aborted with the runtime).
|
|
210
|
+
|
|
211
|
+
### Remote file access (§11.3)
|
|
212
|
+
|
|
213
|
+
`fs` events are served in-process (no shell) and answered on
|
|
214
|
+
`POST /api/dsh-computers/agent/fs/result` with the §11.3 field set.
|
|
215
|
+
|
|
216
|
+
| op | request fields | response highlights |
|
|
217
|
+
| --- | --- | --- |
|
|
218
|
+
| `read` | `path`, `offset?`, `limit?`, `encoding?` (`utf8`/`base64`) | `encoding`, `text` (utf8) or `content` (base64), `bytes`, `lines`, `truncated` |
|
|
219
|
+
| `write` | `path`, `content`, `encoding?`, `createDirs?` | `bytes`, `created` |
|
|
220
|
+
| `list` | `path` | `entries[{name,path,type,size,mtimeMs}]`, `truncated` |
|
|
221
|
+
| `stat` | `path` | `stat{type,size,mtimeMs,mode}` |
|
|
222
|
+
| `mkdir` | `path`, `recursive?` | `created` |
|
|
223
|
+
| `rename` | `path`, `to` | `path` (new) |
|
|
224
|
+
| `delete` | `path`, `recursive?` | `ok` |
|
|
225
|
+
| `glob` | `pattern` (`*`, `?`, `**`, `{a,b}`) | `paths`, `truncated` |
|
|
226
|
+
| `grep` | `pattern` (RegExp), `path?`, `include?` (glob), `maxMatches?` | `matches[{path,line,text}]`, `truncated` |
|
|
227
|
+
|
|
228
|
+
Rules and limits:
|
|
229
|
+
|
|
230
|
+
- `read`/`write`/`list`/`stat`/`mkdir`/`rename`/`delete` require **absolute**
|
|
231
|
+
paths (`ok:false, error:"path must be absolute"`); `glob`/`grep` accept
|
|
232
|
+
relative paths/patterns resolved against `workspaceBase`.
|
|
233
|
+
- read 1 MiB / 20 000 lines, write 4 MiB, list 5 000 entries, glob 2 000
|
|
234
|
+
paths, grep 500 matches, 15 s per request (a timed-out request answers
|
|
235
|
+
`ok:false, error:"request timed out"`).
|
|
236
|
+
- Text vs binary: a NUL byte in the first 8 KiB (or `encoding:"base64"`) makes
|
|
237
|
+
the read return base64 `content`; everything else returns utf8 `text`.
|
|
238
|
+
- Symlinks: `read` follows them like `cat`; `stat`/`list` report the link
|
|
239
|
+
itself; directory walks never follow symlinked directories (loop safety) and
|
|
240
|
+
skip `.git`; `write` follows an existing symlink.
|
|
241
|
+
- grep skips binary files and files larger than 8 MiB; `include` matches the
|
|
242
|
+
basename or workspace-relative path.
|
|
243
|
+
- OS errors are returned verbatim in `error` (e.g. `ENOENT: no such file or
|
|
244
|
+
directory, open '…'`); a request never throws out of the loop.
|
|
245
|
+
|
|
246
|
+
`startRuntime().status()` additionally exposes `workspaceBase`, `capabilities`
|
|
247
|
+
and the last advertised `features`.
|
|
248
|
+
|
|
158
249
|
## Files and environment
|
|
159
250
|
|
|
160
251
|
| Path | Purpose |
|
|
161
252
|
| --- | --- |
|
|
162
|
-
| `${DSH_COMPUTERS_HOME:-$HOME/.dsh-computers}/config.json` | Credentials
|
|
253
|
+
| `${DSH_COMPUTERS_HOME:-$HOME/.dsh-computers}/config.json` | Credentials + `workspaceBase`, mode `0600`, atomic tmp+rename |
|
|
163
254
|
| `${DSH_COMPUTERS_HOME}/agent.log` | Append-only log, rotates at 5 MiB (keeps one `agent.log.1`) |
|
|
164
255
|
| `~/Library/LaunchAgents/com.dsh.computers.agent.plist` | macOS service unit |
|
|
165
256
|
| `~/.config/systemd/user/dsh-computers-agent.service` | Linux service unit |
|
|
@@ -178,7 +269,13 @@ Environment variables:
|
|
|
178
269
|
- **Token**: generated by the server, stored only on this machine in a `0600`
|
|
179
270
|
file, sent as `authorization: Bearer …` on every request. It is never printed,
|
|
180
271
|
never logged, and `status` shows only its last 6 characters.
|
|
181
|
-
- **Outbound only**: enroll, SSE stream, result POSTs, heartbeats
|
|
272
|
+
- **Outbound only**: enroll, SSE stream, result POSTs, heartbeats, chunk/fs
|
|
273
|
+
result POSTs. No listener.
|
|
274
|
+
- **Remote files**: when the server advertises the `fs` feature it can read and
|
|
275
|
+
write files as your user, but only through the ops above; paths outside
|
|
276
|
+
`workspaceBase` are allowed when absolute (documented §11.3 semantics), while
|
|
277
|
+
relative paths are anchored at `workspaceBase`. Directory walks skip `.git`
|
|
278
|
+
and symlinked directories.
|
|
182
279
|
- **Execution identity**: commands run as your user through
|
|
183
280
|
`/bin/sh -lc '<command>'` (Windows: `cmd.exe /d /s /c`), inheriting your
|
|
184
281
|
environment — the same power as your own terminal. Only connect your server
|
|
@@ -213,6 +310,7 @@ removed).
|
|
|
213
310
|
| `launchctl bootstrap` fails (`5: Input/output error` / domain unsupported) | The `gui/<uid>` domain is unavailable — usually an SSH session without a GUI login. The agent falls back to `launchctl load -w`; if both fail, run `service start` once from a logged-in desktop terminal. |
|
|
214
311
|
| `✗ 需要 Node.js >= 22` | Upgrade Node; the script relies on global `fetch` and modern ESM. |
|
|
215
312
|
| Computer shows offline in the UI | Check `agent.log` for reconnect lines; the stream reconnects with 1 s → 30 s backoff. |
|
|
313
|
+
| Streaming/file tools unavailable after upgrading | The host must advertise `stream`/`fs` in `hello.features`; old hosts get v1 behaviour by design (§11.5). The agent log line `已连接 … features=…` shows what the host offered. |
|
|
216
314
|
|
|
217
315
|
## Protocol notes (intentional choices)
|
|
218
316
|
|
|
@@ -273,4 +371,13 @@ paths:
|
|
|
273
371
|
`running: false, connected: false, exitCode: 0`;
|
|
274
372
|
- a foreign bare/other-JSON 401 is retried and the machine reconnects (throttled
|
|
275
373
|
single warning), while JSON `invalid-token` exits 1 without retry;
|
|
276
|
-
- 404/503 during connect are retried and the stream recovers
|
|
374
|
+
- 404/503 during connect are retried and the stream recovers;
|
|
375
|
+
- §11: enroll reports `capabilities:['exec','fs']` + full handshake metadata,
|
|
376
|
+
the metadata header is on every SSE connect, `exec` defaults to
|
|
377
|
+
`workspaceBase`, typed chunk frames arrive with contiguous `seq` (stdout,
|
|
378
|
+
stderr and silent-command heartbeats), chunk POSTs retry once / drop without
|
|
379
|
+
killing the command, an old host (no `features`) gets no chunks and no fs,
|
|
380
|
+
and the fs ops round-trip (read text/binary + windows + limits, relative-path
|
|
381
|
+
rejection, write/list/stat/mkdir/rename/delete, glob with `**` and `{a,b}`,
|
|
382
|
+
grep with `include`/`maxMatches`/binary skip, and `stop()` during an
|
|
383
|
+
in-flight fs request leaves no result POST).
|