moshcode 0.90.0 → 0.91.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/README.md +91 -0
- package/bin/moshcode.mjs +7 -0
- package/package.json +1 -1
- package/prd/0013-persistent-ssh-workspaces.md +1181 -0
- package/prd/README.md +2 -0
- package/src/cli-schema.mjs +109 -0
- package/src/commands.mjs +164 -0
- package/src/ssh.mjs +1228 -0
- package/src/tui.mjs +14 -0
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
---
|
|
2
|
+
openprd: "0.2"
|
|
3
|
+
id: "0013"
|
|
4
|
+
title: "Add persistent SSH workspaces for humans and agents"
|
|
5
|
+
status: "Draft"
|
|
6
|
+
authors:
|
|
7
|
+
- "anthony@profullstack.com"
|
|
8
|
+
created: "2026-09-03"
|
|
9
|
+
updated: "2026-09-03"
|
|
10
|
+
repo: "https://github.com/moshcoder/moshcode"
|
|
11
|
+
discussion: ""
|
|
12
|
+
implementation: "src/ssh.mjs, src/cli-schema.mjs, src/commands.mjs, src/tui.mjs, bin/moshcode.mjs, test/ssh.test.mjs, test/ssh-sshd.test.mjs"
|
|
13
|
+
tags:
|
|
14
|
+
- ssh
|
|
15
|
+
- remote
|
|
16
|
+
- runtime
|
|
17
|
+
- agents
|
|
18
|
+
- chovy
|
|
19
|
+
- workspaces
|
|
20
|
+
supersedes: ""
|
|
21
|
+
superseded-by: ""
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Add persistent SSH workspaces for humans and agents
|
|
25
|
+
|
|
26
|
+
## Problem
|
|
27
|
+
|
|
28
|
+
MoshCode already has a strong persistent-local-runtime model:
|
|
29
|
+
|
|
30
|
+
- `herd` keeps local shells and agents alive in tmux or the PTY fallback.
|
|
31
|
+
- `herd prompt`, `read`, `wait`, and `--json` make those sessions controllable by another agent.
|
|
32
|
+
- `shell()` and the pit's shell execution path deliberately use the user's real shell.
|
|
33
|
+
- the project remains zero-dependency ESM and delegates terminal/process behavior to native tools instead of embedding a terminal implementation.
|
|
34
|
+
|
|
35
|
+
What it does **not** have is a first-class remote-shell/workspace abstraction.
|
|
36
|
+
|
|
37
|
+
Today a caller such as Chovy can invoke the system `ssh` command repeatedly, but if every file read, file edit, `git status`, test run, or inspection starts a brand-new SSH process with a brand-new transport, it repeatedly pays for:
|
|
38
|
+
|
|
39
|
+
- TCP setup;
|
|
40
|
+
- SSH negotiation;
|
|
41
|
+
- host-key negotiation;
|
|
42
|
+
- authentication;
|
|
43
|
+
- key-agent interaction;
|
|
44
|
+
- session setup;
|
|
45
|
+
- remote-shell startup.
|
|
46
|
+
|
|
47
|
+
That is unnecessary. SSH is explicitly capable of carrying many independent channels over one authenticated transport.
|
|
48
|
+
|
|
49
|
+
The immediate Chovy use case is concrete: an AI coding run may inspect and modify dozens or hundreds of files on one remote workspace. Chovy should not create a completely new authenticated SSH transport for every operation.
|
|
50
|
+
|
|
51
|
+
A naive `/ssh` command that merely does this:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
ssh user@host
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
would not solve the problem. The user can already type that as a normal shell command.
|
|
58
|
+
|
|
59
|
+
The useful feature is instead a **persistent SSH workspace manager**:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
one authenticated OpenSSH master connection
|
|
63
|
+
│
|
|
64
|
+
├── exec channel → git status
|
|
65
|
+
├── exec channel → cat package.json
|
|
66
|
+
├── exec channel → apply a multi-file patch
|
|
67
|
+
├── exec channel → pnpm test
|
|
68
|
+
├── scp/sftp-style file transfer
|
|
69
|
+
└── optional remote tmux shell
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Each operation remains independently observable and machine-readable while reusing the same authenticated connection.
|
|
73
|
+
|
|
74
|
+
This is especially useful for AI systems. Models generally work better with discrete tool calls returning bounded stdout/stderr and exit status than by pretending to be a human typing blindly into one long terminal stream. A persistent transport should therefore **not** imply that all AI activity must share one stateful shell.
|
|
75
|
+
|
|
76
|
+
MoshCode needs both:
|
|
77
|
+
|
|
78
|
+
1. multiplexed stateless command channels over one SSH connection; and
|
|
79
|
+
2. an optional persistent interactive remote shell for workflows that actually require shell state, a REPL, a TUI, a dev server, or a long-running process.
|
|
80
|
+
|
|
81
|
+
## Source Review / Why This Fits MoshCode
|
|
82
|
+
|
|
83
|
+
The current codebase already contains most of the concepts needed for this feature.
|
|
84
|
+
|
|
85
|
+
### Existing shell abstraction
|
|
86
|
+
|
|
87
|
+
`src/shell.mjs` centralizes how the pit runs shell commands, including interactive rc-file behavior and terminal/job-control details. The proposed SSH implementation should follow the same principle: one authoritative module should own SSH invocation construction and lifecycle behavior.
|
|
88
|
+
|
|
89
|
+
### Existing persistent runtime
|
|
90
|
+
|
|
91
|
+
`src/herd.mjs` already:
|
|
92
|
+
|
|
93
|
+
- detects tmux and PTY capabilities;
|
|
94
|
+
- creates named persistent sessions;
|
|
95
|
+
- captures output;
|
|
96
|
+
- sends literal input;
|
|
97
|
+
- attaches a terminal;
|
|
98
|
+
- keeps session metadata;
|
|
99
|
+
- exposes machine-readable controls.
|
|
100
|
+
|
|
101
|
+
The SSH feature should reuse the **design philosophy**, not tunnel every SSH command through herd.
|
|
102
|
+
|
|
103
|
+
### Existing machine interface
|
|
104
|
+
|
|
105
|
+
The herd intentionally has no separate hidden API: CLI verbs use `--json`, and scripts/agents consume the same concepts. `/ssh` should follow that rule.
|
|
106
|
+
|
|
107
|
+
### Existing zero-dependency posture
|
|
108
|
+
|
|
109
|
+
MoshCode is deliberately zero-dependency ESM. Do **not** add `ssh2`, `node-pty`, libssh bindings, or a custom SSH protocol implementation.
|
|
110
|
+
|
|
111
|
+
Use the installed OpenSSH client and its native connection-multiplexing support.
|
|
112
|
+
|
|
113
|
+
## Goals
|
|
114
|
+
|
|
115
|
+
- Allow many remote commands to reuse one authenticated SSH transport.
|
|
116
|
+
- Make remote execution dramatically cheaper than reconnecting for every command.
|
|
117
|
+
- Give humans a natural `/ssh` pit command and `moshcode ssh` CLI.
|
|
118
|
+
- Give AI systems a structured, machine-readable `ssh exec` interface.
|
|
119
|
+
- Preserve discrete stdout, stderr, exit status, timeout, and cancellation behavior for each operation.
|
|
120
|
+
- Support stdin so an agent can apply a multi-file patch in one remote operation.
|
|
121
|
+
- Support an optional truly persistent remote shell when shell state matters.
|
|
122
|
+
- Reuse the user's existing OpenSSH configuration, ssh-agent, known_hosts, ProxyJump, identities, and hardware-backed keys.
|
|
123
|
+
- Keep credentials and private keys out of MoshCode storage.
|
|
124
|
+
- Keep the project zero-dependency ESM.
|
|
125
|
+
- Fail soft when OpenSSH or an optional remote capability such as tmux is unavailable.
|
|
126
|
+
- Make the feature directly useful to Chovy without making Chovy depend on MoshCode internals.
|
|
127
|
+
|
|
128
|
+
## Non-Goals
|
|
129
|
+
|
|
130
|
+
- Implement the SSH protocol in JavaScript.
|
|
131
|
+
- Replace OpenSSH.
|
|
132
|
+
- Store SSH passwords.
|
|
133
|
+
- Store private keys.
|
|
134
|
+
- Disable host-key checking.
|
|
135
|
+
- Invent a second `~/.ssh/config`.
|
|
136
|
+
- Force every remote command through one interactive PTY.
|
|
137
|
+
- Require MoshCode to be installed on the remote host.
|
|
138
|
+
- Require tmux for normal `ssh exec`.
|
|
139
|
+
- Build a remote filesystem/FUSE mount.
|
|
140
|
+
- Build an IDE file browser.
|
|
141
|
+
- Replace rsync, scp, or sftp.
|
|
142
|
+
- Automatically deploy MoshCode to remote machines.
|
|
143
|
+
- Make SSH itself an A2A protocol.
|
|
144
|
+
- Treat a remote shell as an AI agent when it is not one.
|
|
145
|
+
|
|
146
|
+
## Users
|
|
147
|
+
|
|
148
|
+
### Chovy / agentic application backend
|
|
149
|
+
|
|
150
|
+
Needs to perform many file and shell operations against one remote app workspace during a coding run without opening a new authenticated SSH transport every time.
|
|
151
|
+
|
|
152
|
+
### MoshCode operator
|
|
153
|
+
|
|
154
|
+
Wants to type:
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
/ssh dev
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
and land on a configured remote box, or:
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
/ssh exec dev -- git status
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
without thinking about connection multiplexing.
|
|
167
|
+
|
|
168
|
+
### Coding agent
|
|
169
|
+
|
|
170
|
+
Needs deterministic tools such as:
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
moshcode ssh exec dev --json -- git diff --stat
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
rather than scraping an interactive terminal.
|
|
177
|
+
|
|
178
|
+
### Automation / moshscript
|
|
179
|
+
|
|
180
|
+
Needs to open a connection, execute several operations, branch on exit codes, and close or leave the connection available for later reuse.
|
|
181
|
+
|
|
182
|
+
## Product Principle
|
|
183
|
+
|
|
184
|
+
**Persistent connection, discrete operations.**
|
|
185
|
+
|
|
186
|
+
The transport stays alive. Commands do not have to share shell state.
|
|
187
|
+
|
|
188
|
+
This is the default:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
AI
|
|
192
|
+
│
|
|
193
|
+
├─ exec("pwd") ───────────────┐
|
|
194
|
+
├─ exec("git status") ────────┤
|
|
195
|
+
├─ exec("git apply -", stdin) ┤
|
|
196
|
+
└─ exec("pnpm test") ─────────┤
|
|
197
|
+
▼
|
|
198
|
+
one OpenSSH master
|
|
199
|
+
│
|
|
200
|
+
▼
|
|
201
|
+
remote server
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Use a stateful remote shell only when the task truly needs one:
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
AI / human
|
|
208
|
+
│
|
|
209
|
+
▼
|
|
210
|
+
remote tmux shell
|
|
211
|
+
│
|
|
212
|
+
├── cd persists
|
|
213
|
+
├── exports persist
|
|
214
|
+
├── dev server persists
|
|
215
|
+
├── REPL persists
|
|
216
|
+
└── TUI persists
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Requirements
|
|
220
|
+
|
|
221
|
+
### Phase 1 — Named SSH targets
|
|
222
|
+
|
|
223
|
+
- **R1 [P0]** Add a core `moshcode ssh` command and `/ssh` pit command.
|
|
224
|
+
|
|
225
|
+
- **R2 [P0]** Support named targets:
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
moshcode ssh add dev deploy@example.com
|
|
229
|
+
moshcode ssh add dev deploy@example.com --port 2222
|
|
230
|
+
moshcode ssh add dev deploy@example.com --cwd /srv/app
|
|
231
|
+
moshcode ssh add dev my-ssh-config-host
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
- **R3 [P0]** Store only non-secret metadata under:
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
~/.moshcode/ssh/targets.json
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Suggested shape:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"dev": {
|
|
245
|
+
"target": "deploy@example.com",
|
|
246
|
+
"port": 22,
|
|
247
|
+
"cwd": "/srv/app"
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- **R4 [P0]** `targets.json` MUST NOT contain:
|
|
253
|
+
- passwords;
|
|
254
|
+
- private-key contents;
|
|
255
|
+
- passphrases;
|
|
256
|
+
- ssh-agent material;
|
|
257
|
+
- temporary auth tokens.
|
|
258
|
+
|
|
259
|
+
- **R5 [P0]** Allow normal OpenSSH host aliases as targets so existing `~/.ssh/config` remains authoritative:
|
|
260
|
+
|
|
261
|
+
```sshconfig
|
|
262
|
+
Host devbox
|
|
263
|
+
HostName 203.0.113.10
|
|
264
|
+
User deploy
|
|
265
|
+
IdentityFile ~/.ssh/id_ed25519
|
|
266
|
+
ProxyJump bastion
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
then:
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
moshcode ssh add dev devbox
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- **R6 [P0]** Commands:
|
|
276
|
+
|
|
277
|
+
```sh
|
|
278
|
+
moshcode ssh
|
|
279
|
+
moshcode ssh list
|
|
280
|
+
moshcode ssh add <name> <target>
|
|
281
|
+
moshcode ssh remove <name>
|
|
282
|
+
moshcode ssh show <name>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Bare `moshcode ssh` lists configured targets and connection state.
|
|
286
|
+
|
|
287
|
+
- **R7 [P0]** Every non-interactive verb supports `--json`.
|
|
288
|
+
|
|
289
|
+
### Phase 2 — Persistent OpenSSH transport
|
|
290
|
+
|
|
291
|
+
- **R8 [P0]** Use native OpenSSH connection multiplexing.
|
|
292
|
+
|
|
293
|
+
MoshCode MUST establish a control master rather than keeping a Node child process with a hand-rolled protocol.
|
|
294
|
+
|
|
295
|
+
Conceptually:
|
|
296
|
+
|
|
297
|
+
```sh
|
|
298
|
+
ssh \
|
|
299
|
+
-o ControlMaster=yes \
|
|
300
|
+
-o ControlPersist=10m \
|
|
301
|
+
-o ControlPath=<moshcode-control-socket> \
|
|
302
|
+
-N -f \
|
|
303
|
+
devbox
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- **R9 [P0]** Add:
|
|
307
|
+
|
|
308
|
+
```sh
|
|
309
|
+
moshcode ssh open <name>
|
|
310
|
+
moshcode ssh check <name>
|
|
311
|
+
moshcode ssh close <name>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Pit equivalents:
|
|
315
|
+
|
|
316
|
+
```text
|
|
317
|
+
/ssh open dev
|
|
318
|
+
/ssh check dev
|
|
319
|
+
/ssh close dev
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
- **R10 [P0]** Opening an already-live master is idempotent and returns success with `alreadyOpen: true`.
|
|
323
|
+
|
|
324
|
+
- **R11 [P0]** Connection state MUST be checked using OpenSSH's control operations where supported, e.g. `ssh -O check`.
|
|
325
|
+
|
|
326
|
+
- **R12 [P0]** Closing MUST use OpenSSH's control operation, e.g. `ssh -O exit`, rather than killing arbitrary PIDs.
|
|
327
|
+
|
|
328
|
+
- **R13 [P0]** Default to a finite `ControlPersist` window after the last client disconnects. Initial default: 10 minutes.
|
|
329
|
+
|
|
330
|
+
Configurable by:
|
|
331
|
+
|
|
332
|
+
```sh
|
|
333
|
+
--persist 30m
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
and:
|
|
337
|
+
|
|
338
|
+
```text
|
|
339
|
+
MOSHCODE_SSH_PERSIST=30m
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
- **R14 [P0]** Use connection keepalives appropriate for unattended agents:
|
|
343
|
+
|
|
344
|
+
```text
|
|
345
|
+
ServerAliveInterval=30
|
|
346
|
+
ServerAliveCountMax=3
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
unless the user has explicitly configured alternatives.
|
|
350
|
+
|
|
351
|
+
- **R15 [P0]** If the master dies or a control socket becomes stale, the next operation MUST:
|
|
352
|
+
1. detect the failure;
|
|
353
|
+
2. clean up only MoshCode-owned stale state;
|
|
354
|
+
3. establish a new master;
|
|
355
|
+
4. retry the requested operation once.
|
|
356
|
+
|
|
357
|
+
- **R16 [P0]** Control socket paths MUST avoid Unix-domain-socket path-length failures.
|
|
358
|
+
|
|
359
|
+
Do not derive a long socket filename directly from `user@host:/workspace/path`.
|
|
360
|
+
|
|
361
|
+
Use a stable short hash:
|
|
362
|
+
|
|
363
|
+
```text
|
|
364
|
+
~/.moshcode/ssh/control/7f31a8c2
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
If platform socket limits make even that unsafe, use a private runtime directory such as:
|
|
368
|
+
|
|
369
|
+
```text
|
|
370
|
+
/tmp/moshcode-ssh-<uid>/
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
- **R17 [P0]** Any runtime/control directory containing sockets MUST be mode `0700`.
|
|
374
|
+
|
|
375
|
+
### Phase 3 — Discrete command execution
|
|
376
|
+
|
|
377
|
+
- **R18 [P0]** Add:
|
|
378
|
+
|
|
379
|
+
```sh
|
|
380
|
+
moshcode ssh exec <name> -- <command> [args...]
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Example:
|
|
384
|
+
|
|
385
|
+
```sh
|
|
386
|
+
moshcode ssh exec dev -- git status --short
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
- **R19 [P0]** `ssh exec` MUST automatically reuse the named target's master connection.
|
|
390
|
+
|
|
391
|
+
- **R20 [P0]** `ssh exec` MUST default to **no PTY**.
|
|
392
|
+
|
|
393
|
+
This keeps:
|
|
394
|
+
- stdout deterministic;
|
|
395
|
+
- stderr deterministic;
|
|
396
|
+
- binary-safe stdin possible;
|
|
397
|
+
- automation predictable.
|
|
398
|
+
|
|
399
|
+
- **R21 [P0]** Add `--tty` for commands that require a terminal:
|
|
400
|
+
|
|
401
|
+
```sh
|
|
402
|
+
moshcode ssh exec dev --tty -- sudo systemctl status nginx
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- **R22 [P0]** Return the command's actual remote exit status.
|
|
406
|
+
|
|
407
|
+
- **R23 [P0]** `--json` output shape:
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"ok": true,
|
|
412
|
+
"target": "dev",
|
|
413
|
+
"connected": true,
|
|
414
|
+
"code": 0,
|
|
415
|
+
"signal": null,
|
|
416
|
+
"stdout": " M src/app.ts\n",
|
|
417
|
+
"stderr": "",
|
|
418
|
+
"durationMs": 84
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- **R24 [P0]** Failed remote commands are not transport failures.
|
|
423
|
+
|
|
424
|
+
Example: remote `grep` exits `1`.
|
|
425
|
+
|
|
426
|
+
JSON:
|
|
427
|
+
|
|
428
|
+
```json
|
|
429
|
+
{
|
|
430
|
+
"ok": false,
|
|
431
|
+
"transportOk": true,
|
|
432
|
+
"code": 1
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
This distinction matters to agents.
|
|
437
|
+
|
|
438
|
+
- **R25 [P0]** SSH/network/auth failures MUST be distinguished from remote command failures.
|
|
439
|
+
|
|
440
|
+
Example:
|
|
441
|
+
|
|
442
|
+
```json
|
|
443
|
+
{
|
|
444
|
+
"ok": false,
|
|
445
|
+
"transportOk": false,
|
|
446
|
+
"code": 255,
|
|
447
|
+
"error": "ssh authentication failed"
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
- **R26 [P0]** Support timeout:
|
|
452
|
+
|
|
453
|
+
```sh
|
|
454
|
+
moshcode ssh exec dev --timeout 2m -- pnpm test
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
- **R27 [P0]** Support per-operation cwd:
|
|
458
|
+
|
|
459
|
+
```sh
|
|
460
|
+
moshcode ssh exec dev --cwd /srv/app -- git status
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
If omitted, use the target's configured default cwd.
|
|
464
|
+
|
|
465
|
+
- **R28 [P1]** Support per-operation environment values:
|
|
466
|
+
|
|
467
|
+
```sh
|
|
468
|
+
moshcode ssh exec dev --env NODE_ENV=test -- pnpm test
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
These values apply to that operation only.
|
|
472
|
+
|
|
473
|
+
- **R29 [P0]** Do not emulate shell persistence for `exec`.
|
|
474
|
+
|
|
475
|
+
This should **not** work by accident:
|
|
476
|
+
|
|
477
|
+
```sh
|
|
478
|
+
moshcode ssh exec dev -- cd /tmp
|
|
479
|
+
moshcode ssh exec dev -- pwd
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
The second command should still use the configured/default cwd.
|
|
483
|
+
|
|
484
|
+
Persistent shell state belongs to the shell-session feature.
|
|
485
|
+
|
|
486
|
+
### Phase 4 — stdin and agent-friendly file editing
|
|
487
|
+
|
|
488
|
+
- **R30 [P0]** `ssh exec` MUST be able to forward stdin.
|
|
489
|
+
|
|
490
|
+
Example:
|
|
491
|
+
|
|
492
|
+
```sh
|
|
493
|
+
printf '%s\n' "$PATCH" |
|
|
494
|
+
moshcode ssh exec dev --stdin --cwd /srv/app -- git apply -
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
- **R31 [P0]** stdin MUST remain raw and must not be shell-escaped, JSON-encoded, line-split, or interpreted by MoshCode.
|
|
498
|
+
|
|
499
|
+
- **R32 [P0]** This is the recommended Chovy multi-file-edit path:
|
|
500
|
+
|
|
501
|
+
```text
|
|
502
|
+
model produces unified diff
|
|
503
|
+
│
|
|
504
|
+
▼
|
|
505
|
+
one `ssh exec --stdin`
|
|
506
|
+
│
|
|
507
|
+
▼
|
|
508
|
+
`git apply -`
|
|
509
|
+
│
|
|
510
|
+
▼
|
|
511
|
+
many files changed atomically-ish in one remote command
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
This is preferable to one SSH operation per changed file when the model already has a patch.
|
|
515
|
+
|
|
516
|
+
- **R33 [P1]** Add convenience transfer verbs backed by OpenSSH-native tools:
|
|
517
|
+
|
|
518
|
+
```sh
|
|
519
|
+
moshcode ssh put dev ./local-file /srv/app/file
|
|
520
|
+
moshcode ssh get dev /srv/app/file ./local-file
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Implementation may use `scp` with the same ControlPath.
|
|
524
|
+
|
|
525
|
+
- **R34 [P1]** `put` SHOULD support atomic replacement for individual files:
|
|
526
|
+
1. copy to a temporary sibling path;
|
|
527
|
+
2. rename on the remote filesystem.
|
|
528
|
+
|
|
529
|
+
- **R35 [P1]** No custom SFTP implementation.
|
|
530
|
+
|
|
531
|
+
### Phase 5 — Interactive connection
|
|
532
|
+
|
|
533
|
+
- **R36 [P0]** Bare named target attaches a normal interactive SSH session:
|
|
534
|
+
|
|
535
|
+
```sh
|
|
536
|
+
moshcode ssh dev
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
pit:
|
|
540
|
+
|
|
541
|
+
```text
|
|
542
|
+
/ssh dev
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
- **R37 [P0]** Interactive attach MUST reuse the same ControlMaster when available.
|
|
546
|
+
|
|
547
|
+
- **R38 [P0]** Interactive mode hands the terminal directly to OpenSSH. MoshCode does not parse or redraw the remote terminal.
|
|
548
|
+
|
|
549
|
+
- **R39 [P0]** Ctrl-C, terminal resize, colors, mouse input, TUIs, vim, top, btop, and nested agent CLIs should behave as they do under ordinary OpenSSH.
|
|
550
|
+
|
|
551
|
+
- **R40 [P0]** Exiting the interactive shell does **not** necessarily close the master connection. `ControlPersist` governs transport lifetime.
|
|
552
|
+
|
|
553
|
+
### Phase 6 — Persistent stateful remote shell
|
|
554
|
+
|
|
555
|
+
A multiplexed SSH connection avoids repeated authentication, but separate `exec` channels intentionally do not preserve shell state.
|
|
556
|
+
|
|
557
|
+
Some workflows need actual state:
|
|
558
|
+
|
|
559
|
+
```sh
|
|
560
|
+
cd /srv/app
|
|
561
|
+
export DEBUG=1
|
|
562
|
+
pnpm dev
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
or an interactive CLI that stays alive.
|
|
566
|
+
|
|
567
|
+
- **R41 [P1]** Add:
|
|
568
|
+
|
|
569
|
+
```sh
|
|
570
|
+
moshcode ssh shell <target> --name <session>
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Example:
|
|
574
|
+
|
|
575
|
+
```sh
|
|
576
|
+
moshcode ssh shell dev --name app
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
- **R42 [P1]** When remote tmux exists, create-or-attach a namespaced remote tmux session:
|
|
580
|
+
|
|
581
|
+
```text
|
|
582
|
+
moshcode-ssh-<local-target>-<session>
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
- **R43 [P1]** The remote shell persists independently of the local terminal and independently of the local SSH transport. If the laptop sleeps, the remote tmux shell remains.
|
|
586
|
+
|
|
587
|
+
- **R44 [P1]** Add non-attaching machine controls:
|
|
588
|
+
|
|
589
|
+
```sh
|
|
590
|
+
moshcode ssh shell send dev/app "pnpm test"
|
|
591
|
+
moshcode ssh shell read dev/app --lines 80
|
|
592
|
+
moshcode ssh shell kill dev/app
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
- **R45 [P1]** `send` MUST write literal text followed by Enter, matching herd's existing literal-input safety model.
|
|
596
|
+
|
|
597
|
+
- **R46 [P1]** `read` uses remote `tmux capture-pane`, returning terminal text rather than requiring the caller to attach.
|
|
598
|
+
|
|
599
|
+
- **R47 [P1]** The remote shell feature MUST gracefully report when tmux is unavailable on the remote machine.
|
|
600
|
+
|
|
601
|
+
Normal `ssh exec` remains fully functional.
|
|
602
|
+
|
|
603
|
+
- **R48 [P1]** Do not silently install tmux remotely.
|
|
604
|
+
|
|
605
|
+
### Phase 7 — Moshscript / agent API
|
|
606
|
+
|
|
607
|
+
- **R49 [P0]** Expose value-returning moshscript helpers instead of forcing scripts to parse human text.
|
|
608
|
+
|
|
609
|
+
Proposed:
|
|
610
|
+
|
|
611
|
+
```js
|
|
612
|
+
sshOpen("dev");
|
|
613
|
+
const r = sshExec("dev", ["git", "status", "--short"], {
|
|
614
|
+
cwd: "/srv/app"
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
if (!r.ok) {
|
|
618
|
+
say(r.stderr);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
sshClose("dev");
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
- **R50 [P0]** `sshExec()` returns the same conceptual object as CLI `--json`:
|
|
625
|
+
|
|
626
|
+
```js
|
|
627
|
+
{
|
|
628
|
+
ok,
|
|
629
|
+
transportOk,
|
|
630
|
+
code,
|
|
631
|
+
signal,
|
|
632
|
+
stdout,
|
|
633
|
+
stderr,
|
|
634
|
+
durationMs
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
- **R51 [P1]** Support stdin in moshscript:
|
|
639
|
+
|
|
640
|
+
```js
|
|
641
|
+
sshExec("dev", ["git", "apply", "-"], {
|
|
642
|
+
cwd: "/srv/app",
|
|
643
|
+
stdin: patch
|
|
644
|
+
});
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
- **R52 [P1]** Add shell-session helpers only if Phase 6 is implemented:
|
|
648
|
+
|
|
649
|
+
```js
|
|
650
|
+
sshShellSend("dev/app", "pnpm test");
|
|
651
|
+
const screen = sshShellRead("dev/app", { lines: 50 });
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
(Starting a shell hands the terminal to ssh, so it is a CLI verb rather
|
|
655
|
+
than a script helper; a script drives an existing shell with `send`,
|
|
656
|
+
`read` and `kill`.)
|
|
657
|
+
|
|
658
|
+
### Phase 8 — Chovy integration contract
|
|
659
|
+
|
|
660
|
+
The feature should be usable by Chovy strictly through the public CLI. Chovy must not import MoshCode private modules.
|
|
661
|
+
|
|
662
|
+
Recommended lifecycle:
|
|
663
|
+
|
|
664
|
+
```sh
|
|
665
|
+
# once when a workspace is provisioned
|
|
666
|
+
moshcode ssh add chovy-app app@server --cwd /srv/chovy/workspace
|
|
667
|
+
|
|
668
|
+
# once at the beginning of an active coding run
|
|
669
|
+
moshcode ssh open chovy-app --json
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Then every AI tool operation:
|
|
673
|
+
|
|
674
|
+
```sh
|
|
675
|
+
moshcode ssh exec chovy-app --json -- git status --short
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Read a file:
|
|
679
|
+
|
|
680
|
+
```sh
|
|
681
|
+
moshcode ssh exec chovy-app --json -- sed -n '1,240p' src/app.ts
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Apply a model-generated multi-file patch:
|
|
685
|
+
|
|
686
|
+
```sh
|
|
687
|
+
moshcode ssh exec chovy-app \
|
|
688
|
+
--json \
|
|
689
|
+
--stdin \
|
|
690
|
+
--cwd /srv/chovy/workspace \
|
|
691
|
+
-- git apply -
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Run tests:
|
|
695
|
+
|
|
696
|
+
```sh
|
|
697
|
+
moshcode ssh exec chovy-app \
|
|
698
|
+
--json \
|
|
699
|
+
--timeout 10m \
|
|
700
|
+
--cwd /srv/chovy/workspace \
|
|
701
|
+
-- pnpm test
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Optional interactive debug:
|
|
705
|
+
|
|
706
|
+
```sh
|
|
707
|
+
moshcode ssh chovy-app
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
Optional persistent remote dev shell:
|
|
711
|
+
|
|
712
|
+
```sh
|
|
713
|
+
moshcode ssh shell chovy-app --name dev
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
At run completion:
|
|
717
|
+
|
|
718
|
+
```sh
|
|
719
|
+
moshcode ssh close chovy-app
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
or simply let `ControlPersist` expire.
|
|
723
|
+
|
|
724
|
+
- **R53 [P0]** Chovy SHOULD keep using discrete model tool calls.
|
|
725
|
+
|
|
726
|
+
- **R54 [P0]** Chovy SHOULD NOT force all model actions through a single interactive shell merely to avoid reconnect cost.
|
|
727
|
+
|
|
728
|
+
- **R55 [P0]** Chovy SHOULD batch model file changes into unified diffs where practical and apply one patch through stdin.
|
|
729
|
+
|
|
730
|
+
- **R56 [P0]** Chovy MAY run independent commands concurrently over the same master connection. SSH multiplexing should allow several logical channels over one authenticated transport.
|
|
731
|
+
|
|
732
|
+
- **R57 [P0]** A single Chovy coding run should normally perform one SSH authentication/transport setup, not one per file.
|
|
733
|
+
|
|
734
|
+
## UX Notes
|
|
735
|
+
|
|
736
|
+
### Human pit flow
|
|
737
|
+
|
|
738
|
+
```text
|
|
739
|
+
mosh ▸ /ssh add dev deploy@dev.example.com --cwd ~/src/app
|
|
740
|
+
✓ dev → deploy@dev.example.com
|
|
741
|
+
|
|
742
|
+
mosh ▸ /ssh open dev
|
|
743
|
+
✓ dev connected
|
|
744
|
+
|
|
745
|
+
mosh ▸ /ssh exec dev -- git status --short
|
|
746
|
+
M src/app.ts
|
|
747
|
+
|
|
748
|
+
mosh ▸ /ssh dev
|
|
749
|
+
deploy@dev:~/src/app$
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
Leaving the remote shell returns to the pit while the master connection remains reusable.
|
|
753
|
+
|
|
754
|
+
### Connection list
|
|
755
|
+
|
|
756
|
+
```text
|
|
757
|
+
mosh ▸ /ssh
|
|
758
|
+
|
|
759
|
+
name target state cwd
|
|
760
|
+
dev deploy@dev.example.com connected ~/src/app
|
|
761
|
+
prod deploy@prod.example.com closed /srv/app
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### JSON list
|
|
765
|
+
|
|
766
|
+
```json
|
|
767
|
+
{
|
|
768
|
+
"targets": [
|
|
769
|
+
{
|
|
770
|
+
"name": "dev",
|
|
771
|
+
"target": "deploy@dev.example.com",
|
|
772
|
+
"connected": true,
|
|
773
|
+
"cwd": "~/src/app"
|
|
774
|
+
}
|
|
775
|
+
]
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Shell session
|
|
780
|
+
|
|
781
|
+
```text
|
|
782
|
+
mosh ▸ /ssh shell dev --name app
|
|
783
|
+
dev/app ▸ ~/src/app
|
|
784
|
+
|
|
785
|
+
deploy@dev:~/src/app$ pnpm dev
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
Detach behavior should be documented clearly. If remote tmux is used, detaching must leave the remote process alive.
|
|
789
|
+
|
|
790
|
+
## CLI Surface
|
|
791
|
+
|
|
792
|
+
```text
|
|
793
|
+
moshcode ssh
|
|
794
|
+
moshcode ssh list
|
|
795
|
+
moshcode ssh add <name> <target> [--port N] [--cwd PATH]
|
|
796
|
+
moshcode ssh remove <name>
|
|
797
|
+
moshcode ssh show <name>
|
|
798
|
+
|
|
799
|
+
moshcode ssh open <name> [--persist 10m]
|
|
800
|
+
moshcode ssh check <name>
|
|
801
|
+
moshcode ssh close <name>
|
|
802
|
+
|
|
803
|
+
moshcode ssh <name>
|
|
804
|
+
moshcode ssh exec <name> [--cwd PATH] [--env K=V] [--stdin] [--tty] [--timeout DURATION] -- <command...>
|
|
805
|
+
|
|
806
|
+
moshcode ssh put <name> <local> <remote>
|
|
807
|
+
moshcode ssh get <name> <remote> <local>
|
|
808
|
+
|
|
809
|
+
moshcode ssh shell <name> --name <session>
|
|
810
|
+
moshcode ssh shell send <name>/<session> <text>
|
|
811
|
+
moshcode ssh shell read <name>/<session> [--lines N]
|
|
812
|
+
moshcode ssh shell kill <name>/<session>
|
|
813
|
+
|
|
814
|
+
moshcode ssh bench <name> [--n 20]
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
Every appropriate verb:
|
|
818
|
+
|
|
819
|
+
```text
|
|
820
|
+
--json
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
Pit facade:
|
|
824
|
+
|
|
825
|
+
```text
|
|
826
|
+
/ssh ...
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
## Architecture
|
|
830
|
+
|
|
831
|
+
### New module: `src/ssh.mjs`
|
|
832
|
+
|
|
833
|
+
Own:
|
|
834
|
+
|
|
835
|
+
- target registry;
|
|
836
|
+
- control socket naming;
|
|
837
|
+
- OpenSSH capability detection;
|
|
838
|
+
- master open/check/close;
|
|
839
|
+
- invocation construction;
|
|
840
|
+
- exec;
|
|
841
|
+
- stdin forwarding;
|
|
842
|
+
- output capture;
|
|
843
|
+
- timeout;
|
|
844
|
+
- interactive attach;
|
|
845
|
+
- scp convenience;
|
|
846
|
+
- optional remote tmux shell helpers.
|
|
847
|
+
|
|
848
|
+
No SSH command construction should be duplicated in `cli.mjs`, `commands.mjs`, or Chovy.
|
|
849
|
+
|
|
850
|
+
### `src/cli-schema.mjs`
|
|
851
|
+
|
|
852
|
+
Add the canonical help schema for `ssh` and its verbs.
|
|
853
|
+
|
|
854
|
+
The README command table is generated from the command schema, so `/ssh` must enter through the same canonical command/help path as existing commands.
|
|
855
|
+
|
|
856
|
+
### `bin/moshcode.mjs`
|
|
857
|
+
|
|
858
|
+
Dispatch `moshcode ssh ...` into `src/ssh.mjs`.
|
|
859
|
+
|
|
860
|
+
### `src/commands.mjs`
|
|
861
|
+
|
|
862
|
+
Expose:
|
|
863
|
+
|
|
864
|
+
```js
|
|
865
|
+
cliVerb("ssh", "connect to and operate persistent remote SSH workspaces")
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
plus value-returning moshscript helpers where required.
|
|
869
|
+
|
|
870
|
+
### `src/runtime.mjs`
|
|
871
|
+
|
|
872
|
+
The runtime injects every registered command as a global, so the helpers in
|
|
873
|
+
`src/commands.mjs` are the injection; no runtime change is needed.
|
|
874
|
+
|
|
875
|
+
### Relationship to `src/herd.mjs`
|
|
876
|
+
|
|
877
|
+
Phase 1 does not change herd.
|
|
878
|
+
|
|
879
|
+
This is intentional:
|
|
880
|
+
|
|
881
|
+
- herd owns local persistent processes and agent state;
|
|
882
|
+
- ssh owns remote transport and remote command execution.
|
|
883
|
+
|
|
884
|
+
Future integration may allow a remote SSH shell or an SSH-launched remote agent to appear in `moshcode ps`, but `/ssh` should first work cleanly as an independent transport primitive.
|
|
885
|
+
|
|
886
|
+
## OpenSSH Invocation Strategy
|
|
887
|
+
|
|
888
|
+
Implementation builds argv arrays, never a shell string.
|
|
889
|
+
|
|
890
|
+
### Master
|
|
891
|
+
|
|
892
|
+
```sh
|
|
893
|
+
ssh \
|
|
894
|
+
-o ControlMaster=yes \
|
|
895
|
+
-o ControlPersist=600 \
|
|
896
|
+
-o ControlPath=/private/path/abc123 \
|
|
897
|
+
-o ServerAliveInterval=30 \
|
|
898
|
+
-o ServerAliveCountMax=3 \
|
|
899
|
+
-o ConnectTimeout=20 \
|
|
900
|
+
-N -f \
|
|
901
|
+
devbox
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
Note the absence of `-M`. Found in testing: `-M` together with
|
|
905
|
+
`-o ControlMaster=yes` is read by ssh as a *second* request for master mode,
|
|
906
|
+
which means **ask** mode — every later client then needs an askpass
|
|
907
|
+
confirmation, and headless the answer is "Master refused session request:
|
|
908
|
+
Permission denied". One spelling or the other, never both.
|
|
909
|
+
|
|
910
|
+
### Check
|
|
911
|
+
|
|
912
|
+
```sh
|
|
913
|
+
ssh \
|
|
914
|
+
-o ControlPath=/private/path/abc123 \
|
|
915
|
+
-O check \
|
|
916
|
+
devbox
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
### Close
|
|
920
|
+
|
|
921
|
+
```sh
|
|
922
|
+
ssh \
|
|
923
|
+
-o ControlPath=/private/path/abc123 \
|
|
924
|
+
-O exit \
|
|
925
|
+
devbox
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### Exec
|
|
929
|
+
|
|
930
|
+
```sh
|
|
931
|
+
ssh \
|
|
932
|
+
-o ControlPath=/private/path/abc123 \
|
|
933
|
+
-o ControlMaster=auto \
|
|
934
|
+
-o ControlPersist=600 \
|
|
935
|
+
-T \
|
|
936
|
+
devbox \
|
|
937
|
+
-- <remote-command>
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
`ControlMaster=auto` on the client is the native stale-socket recovery: a
|
|
941
|
+
socket nobody is listening on is unlinked and the client becomes the new
|
|
942
|
+
master, so a master that died between two commands costs one reconnect.
|
|
943
|
+
|
|
944
|
+
The remote command is built from argv with POSIX single-quoting:
|
|
945
|
+
|
|
946
|
+
```text
|
|
947
|
+
cd -- '/srv/app' && NODE_ENV='test' exec 'pnpm' 'test'
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
A `--sh` flag passes a single argument as a shell snippet on purpose; nothing
|
|
951
|
+
is ever guessed to be one.
|
|
952
|
+
|
|
953
|
+
## Security
|
|
954
|
+
|
|
955
|
+
- **R58 [P0]** Never pass `StrictHostKeyChecking=no`.
|
|
956
|
+
- **R59 [P0]** Respect normal OpenSSH `known_hosts` behavior.
|
|
957
|
+
- **R60 [P0]** Never persist passwords.
|
|
958
|
+
- **R61 [P0]** Never copy private keys into `~/.moshcode`.
|
|
959
|
+
- **R62 [P0]** Prefer ssh-agent, OpenSSH config, hardware-backed keys, and standard identity files.
|
|
960
|
+
- **R63 [P0]** Redact obvious secret-bearing CLI arguments from debug logs where MoshCode controls logging.
|
|
961
|
+
- **R64 [P0]** Do not print full stdin payloads in debug output.
|
|
962
|
+
- **R65 [P0]** The socket/control directory must be private to the current OS user.
|
|
963
|
+
- **R66 [P0]** Refuse target names containing path separators or traversal components.
|
|
964
|
+
- **R67 [P0]** Registry file writes must be atomic and owner-only.
|
|
965
|
+
- **R68 [P0]** Remote commands must be built from argv with explicit quoting rules.
|
|
966
|
+
- **R69 [P0]** `--env` values must not be echoed in ordinary human output.
|
|
967
|
+
- **R70 [P0]** The feature must not weaken the user's existing SSH policy.
|
|
968
|
+
|
|
969
|
+
## Failure Modes
|
|
970
|
+
|
|
971
|
+
### OpenSSH missing
|
|
972
|
+
|
|
973
|
+
```text
|
|
974
|
+
✗ ssh not found — install an OpenSSH client
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
No package is auto-installed.
|
|
978
|
+
|
|
979
|
+
### Authentication requires interaction
|
|
980
|
+
|
|
981
|
+
Interactive `/ssh dev` may naturally allow OpenSSH to ask.
|
|
982
|
+
|
|
983
|
+
Headless `ssh exec --json` should fail clearly rather than hang indefinitely.
|
|
984
|
+
`BatchMode=yes` is passed whenever stdin is not a terminal or `--batch` is
|
|
985
|
+
given; with a terminal attached, OpenSSH may prompt as it normally would.
|
|
986
|
+
|
|
987
|
+
### Unknown host key
|
|
988
|
+
|
|
989
|
+
Use native OpenSSH behavior. Do not auto-accept.
|
|
990
|
+
|
|
991
|
+
### Stale master socket
|
|
992
|
+
|
|
993
|
+
Detect → clean MoshCode-owned stale socket → reconnect → retry once.
|
|
994
|
+
|
|
995
|
+
### Remote command exits nonzero
|
|
996
|
+
|
|
997
|
+
Return command exit status without calling it an SSH failure.
|
|
998
|
+
|
|
999
|
+
### Remote tmux missing
|
|
1000
|
+
|
|
1001
|
+
Only `ssh shell` persistent mode is unavailable. `ssh exec` still works.
|
|
1002
|
+
|
|
1003
|
+
### Local process dies
|
|
1004
|
+
|
|
1005
|
+
A detached ControlMaster may survive according to OpenSSH behavior and ControlPersist. Remote tmux shells survive regardless of the local master.
|
|
1006
|
+
|
|
1007
|
+
## Performance Expectations
|
|
1008
|
+
|
|
1009
|
+
The feature exists to remove repeated SSH handshakes from high-churn agent workloads.
|
|
1010
|
+
|
|
1011
|
+
### Required measurement
|
|
1012
|
+
|
|
1013
|
+
`moshcode ssh bench <name> [--n N]` compares:
|
|
1014
|
+
|
|
1015
|
+
```text
|
|
1016
|
+
N × fresh ssh "true"
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
against:
|
|
1020
|
+
|
|
1021
|
+
```text
|
|
1022
|
+
1 × master connection
|
|
1023
|
+
N × multiplexed ssh "true"
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
and reports total wall time, median and p95 latency, failures, and the number
|
|
1027
|
+
of authentications each side performed.
|
|
1028
|
+
|
|
1029
|
+
Measured on a loopback sshd on the development box (20 runs each): fresh
|
|
1030
|
+
~96ms median, multiplexed ~12ms median. Real hosts will differ; the number to
|
|
1031
|
+
quote is the one `bench` prints for your own host.
|
|
1032
|
+
|
|
1033
|
+
## Success Metrics
|
|
1034
|
+
|
|
1035
|
+
- A Chovy run that performs 100 remote operations normally authenticates once rather than 100 times.
|
|
1036
|
+
- Median subsequent `ssh exec` startup latency is materially lower than a fresh SSH connection on the same host.
|
|
1037
|
+
- File edits can be applied as one multi-file patch over stdin.
|
|
1038
|
+
- `ssh exec --json` exposes stdout, stderr, exit status, transport status, and duration without terminal scraping.
|
|
1039
|
+
- Interactive `/ssh <name>` behaves like normal OpenSSH.
|
|
1040
|
+
- Existing `~/.ssh/config` features continue to work.
|
|
1041
|
+
- No SSH private key or password is stored by MoshCode.
|
|
1042
|
+
- No runtime npm dependency is added.
|
|
1043
|
+
- MoshCode remains usable when tmux is absent.
|
|
1044
|
+
- Tests cover stale sockets, failed authentication, remote exit codes, stdin, quoting, cwd, and JSON output.
|
|
1045
|
+
|
|
1046
|
+
## Test Plan
|
|
1047
|
+
|
|
1048
|
+
### Unit (`test/ssh.test.mjs`)
|
|
1049
|
+
|
|
1050
|
+
- target-name validation;
|
|
1051
|
+
- registry read/write;
|
|
1052
|
+
- control-path hashing;
|
|
1053
|
+
- OpenSSH argv construction;
|
|
1054
|
+
- port handling;
|
|
1055
|
+
- cwd encoding;
|
|
1056
|
+
- environment encoding;
|
|
1057
|
+
- remote argv quoting;
|
|
1058
|
+
- JSON shapes;
|
|
1059
|
+
- exit-code mapping;
|
|
1060
|
+
- transport-vs-command failure classification;
|
|
1061
|
+
- timeout parsing;
|
|
1062
|
+
- stale-socket recovery decision logic.
|
|
1063
|
+
|
|
1064
|
+
### Integration (`test/ssh-sshd.test.mjs`)
|
|
1065
|
+
|
|
1066
|
+
An ephemeral, non-root `sshd` on a loopback port with generated keys and a
|
|
1067
|
+
private `ssh_config` (pointed at through `MOSHCODE_SSH_CONFIG`). Skipped, not
|
|
1068
|
+
failed, where `sshd` or `ssh-keygen` is unavailable.
|
|
1069
|
+
|
|
1070
|
+
1. add target;
|
|
1071
|
+
2. open master;
|
|
1072
|
+
3. check master;
|
|
1073
|
+
4. exec `printf`;
|
|
1074
|
+
5. exec failing command;
|
|
1075
|
+
6. stdin round trip;
|
|
1076
|
+
7. cwd;
|
|
1077
|
+
8. parallel exec channels;
|
|
1078
|
+
9. close master;
|
|
1079
|
+
10. automatic reopen;
|
|
1080
|
+
11. host-key failure;
|
|
1081
|
+
12. authentication failure;
|
|
1082
|
+
13. `scp` reuse via `put`/`get`.
|
|
1083
|
+
|
|
1084
|
+
### Remote tmux
|
|
1085
|
+
|
|
1086
|
+
When tmux is present in the test image:
|
|
1087
|
+
|
|
1088
|
+
1. send `cd` and `pwd`, verify state persists;
|
|
1089
|
+
2. read the screen;
|
|
1090
|
+
3. kill.
|
|
1091
|
+
|
|
1092
|
+
## Documentation
|
|
1093
|
+
|
|
1094
|
+
README section `## SSH workspaces`, leading with:
|
|
1095
|
+
|
|
1096
|
+
> `/ssh` keeps the SSH connection alive; `ssh exec` still gives each tool call a clean command channel.
|
|
1097
|
+
|
|
1098
|
+
with a Chovy/agent example showing one connection and a multi-file `git apply -`, and `moshcode help ssh`.
|
|
1099
|
+
|
|
1100
|
+
## Rollout
|
|
1101
|
+
|
|
1102
|
+
### Milestone 1
|
|
1103
|
+
|
|
1104
|
+
- target registry;
|
|
1105
|
+
- `/ssh`;
|
|
1106
|
+
- open/check/close;
|
|
1107
|
+
- exec;
|
|
1108
|
+
- JSON;
|
|
1109
|
+
- stdin;
|
|
1110
|
+
- cwd;
|
|
1111
|
+
- tests.
|
|
1112
|
+
|
|
1113
|
+
This alone solves the Chovy reconnect problem.
|
|
1114
|
+
|
|
1115
|
+
### Milestone 2
|
|
1116
|
+
|
|
1117
|
+
- put/get;
|
|
1118
|
+
- timeout polish;
|
|
1119
|
+
- parallel execution tests;
|
|
1120
|
+
- moshscript value helpers.
|
|
1121
|
+
|
|
1122
|
+
### Milestone 3
|
|
1123
|
+
|
|
1124
|
+
- persistent remote tmux shell;
|
|
1125
|
+
- send/read/kill;
|
|
1126
|
+
- optional herd bridge exploration.
|
|
1127
|
+
|
|
1128
|
+
## Future: Herd Bridge
|
|
1129
|
+
|
|
1130
|
+
Do not block this PRD on herd integration.
|
|
1131
|
+
|
|
1132
|
+
A later PRD may define:
|
|
1133
|
+
|
|
1134
|
+
```sh
|
|
1135
|
+
moshcode herd remote add devbox --kind ssh --target dev
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
or allow:
|
|
1139
|
+
|
|
1140
|
+
```sh
|
|
1141
|
+
moshcode ssh agent dev --engine claude --name api
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
to launch a MoshCode herd/agent on a remote machine.
|
|
1145
|
+
|
|
1146
|
+
The clean layering should be:
|
|
1147
|
+
|
|
1148
|
+
```text
|
|
1149
|
+
herd / agent orchestration
|
|
1150
|
+
│
|
|
1151
|
+
▼
|
|
1152
|
+
ssh workspace
|
|
1153
|
+
│
|
|
1154
|
+
▼
|
|
1155
|
+
OpenSSH
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
not:
|
|
1159
|
+
|
|
1160
|
+
```text
|
|
1161
|
+
SSH implementation hidden inside herd
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
## Risks & Open Questions
|
|
1165
|
+
|
|
1166
|
+
- OpenSSH multiplexing behavior differs slightly across platforms. POSIX/OpenSSH-first is acceptable, but capability checks must be explicit.
|
|
1167
|
+
- ControlPath socket limits can be surprisingly small; hashed short paths are mandatory.
|
|
1168
|
+
- Remote argv quoting is security-sensitive and deserves dedicated tests.
|
|
1169
|
+
- `BatchMode=yes` is on whenever stdin is not a terminal, and `--batch` forces it; a person at a terminal can still be prompted.
|
|
1170
|
+
- `ControlPersist=10m` is a reasonable default but Chovy may want a master open for the entire coding-run lifetime. Explicit `open` + `close` already handles that.
|
|
1171
|
+
- `scp` behavior and flags have changed across OpenSSH versions; `put/get` are P1, not required for the core reconnect fix.
|
|
1172
|
+
- Long-running noninteractive commands are still individual channels. If a command must outlive its caller, use remote tmux/systemd/herd rather than pretending `ssh exec` is a job supervisor.
|
|
1173
|
+
- A future remote-herd abstraction should decide whether MoshCode is installed remotely or whether local MoshCode drives raw remote tmux. That decision is intentionally outside this PRD.
|
|
1174
|
+
|
|
1175
|
+
## Decision
|
|
1176
|
+
|
|
1177
|
+
Build `/ssh`, but build it as a **persistent SSH workspace primitive**, not as a convenience alias for `ssh`.
|
|
1178
|
+
|
|
1179
|
+
For Chovy, the key win is not "one forever-interactive shell." The key win is:
|
|
1180
|
+
|
|
1181
|
+
> **one authenticated SSH transport, many clean AI tool calls, plus an optional persistent remote shell when state is actually needed.**
|