@hyzyn/dsh-tty 0.21.0 → 0.22.0-rc.2
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.en.md +113 -10
- package/README.md +117 -10
- package/client.js +150 -36
- package/lib/index.d.ts +195 -25
- package/lib/index.js +930 -149
- package/lib/index.js.map +1 -1
- package/lib/probe.d.ts +29 -1
- package/lib/probe.js +98 -19
- package/lib/probe.js.map +1 -1
- package/lib/sftp.js +37 -9
- package/lib/sftp.js.map +1 -1
- package/lib/shell-integration.js +25 -1
- package/lib/shell-integration.js.map +1 -1
- package/lib/ssh-config.d.ts +34 -2
- package/lib/ssh-config.js +150 -32
- package/lib/ssh-config.js.map +1 -1
- package/lib/ssh.d.ts +256 -2
- package/lib/ssh.js +637 -6
- package/lib/ssh.js.map +1 -1
- package/lib/tunnels.js +47 -5
- package/lib/tunnels.js.map +1 -1
- package/package.json +4 -2
- package/scripts/integration.mjs +183 -20
- package/scripts/jump-smoke.mjs +175 -0
- package/scripts/lib/proxy-bridge.mjs +30 -0
- package/scripts/preview/harness.js +74 -26
- package/scripts/probe-route-smoke.mjs +10 -1
- package/scripts/proxycommand-smoke.mjs +267 -0
- package/scripts/windows-smoke.mjs +123 -1
package/README.en.md
CHANGED
|
@@ -76,6 +76,12 @@ Both were reproduced on **Windows 11 ARM (24H2) + Node 22 ARM64**. The fix:
|
|
|
76
76
|
TERM / COLORTERM are meaningless for ConPTY and are no longer injected;
|
|
77
77
|
- **“Run one command” tabs** (docker exec, command tabs opened by the agent) use `cmd /c` or
|
|
78
78
|
`PowerShell -Command`;
|
|
79
|
+
- **Input line-ending normalisation (D74, reported on real hardware 2026-09-27)**: conhost treats Enter as
|
|
80
|
+
**CR**, so a bare LF only moves the cursor down and **does not submit the command line** — `tty_send`
|
|
81
|
+
writing `echo X\n` as the tool description says leaves the command sitting on the input line (no output,
|
|
82
|
+
no new prompt, looking like “it was sent but never ran”). For **local sessions on win32** the plugin now
|
|
83
|
+
turns a bare LF into CRLF (an existing CRLF is not doubled; non-Windows and SSH sessions pass through
|
|
84
|
+
untouched), so the documented `\n` path just works;
|
|
79
85
|
- **Three things are not supported** (the host turns them off and the settings card explains why):
|
|
80
86
|
- **Shell integration (OSC 133/7)**: injection relies on POSIX rc stubs plus the `-c` wrapper, neither of
|
|
81
87
|
which exists for cmd / PowerShell, so it is permanently off — **local** tabs therefore lose cwd tracking
|
|
@@ -92,7 +98,9 @@ Both were reproduced on **Windows 11 ARM (24H2) + Node 22 ARM64**. The fix:
|
|
|
92
98
|
rebuild), plus `[dsh-tty] mounted (shell=C:\WINDOWS\system32\cmd.exe)`.
|
|
93
99
|
x64 Windows is covered by the CI matrix (build / typecheck / test, see Development).
|
|
94
100
|
- **End-to-end coverage (0.19.0)**: `scripts/windows-smoke.mjs` drives a real `cmd.exe` through
|
|
95
|
-
spawn → input echo → kill → respawn on the CI windows-latest runner
|
|
101
|
+
spawn → input echo → kill → respawn on the CI windows-latest runner, plus two links that only hold on
|
|
102
|
+
a real machine (W6: after normalisation a bare LF really does submit in conhost, D74; W7: a `command`
|
|
103
|
+
session that exits turns into read-only retention and `tty_capture` still reads its pre-exit output, D77). It caught and now pins a
|
|
96
104
|
Windows-only failure class: force-killing a local PTY crashed the host — node-pty rejects signals
|
|
97
105
|
on Windows, and its `_deferNoArgs` re-throws that error from a socket callback, where the caller’s
|
|
98
106
|
try/catch cannot see it.
|
|
@@ -103,14 +111,14 @@ The plugin injects sixteen tools into the agent (with the same power as the bash
|
|
|
103
111
|
|
|
104
112
|
| Tool | Purpose |
|
|
105
113
|
| --- | --- |
|
|
106
|
-
| `tty_list` | List
|
|
107
|
-
| `tty_open` | **Open a terminal session yourself** (0.20.0): a local shell, or a
|
|
108
|
-
| `tty_close` | Close a session opened by `tty_open` (0.20.0). **Only the agent’s own sessions may be closed**: a tab the user opened is refused, so the agent never ends a terminal the user is working in |
|
|
114
|
+
| `tty_list` | List terminal sessions (sid / kind (`local\|ssh`) / target / pid / **cwd tracked live as you `cd`** / activity time; tmux persistent sessions carry a `persist` marker; sessions the agent opened carry `owner: 'agent'`). **Includes sessions whose process has exited but which are still inside their read-only retention window** (`exited:true` + exit code/signal, see below) |
|
|
115
|
+
| `tty_open` | **Open a terminal session yourself** (0.20.0): a local shell, or a command via `command` (dev server / watch; it runs as **a whole piece of shell code** — `cd x && cmd`, `a; b`, even a multi-line script, and it is *not* subject to the “single line ≤2000” client rule below — D78), with optional tmux persistence via `persistName`. The session **shows up in the user’s terminal panel** as an ordinary tab the user can see and take over — never a hidden session |
|
|
116
|
+
| `tty_close` | Close a session opened by `tty_open` (0.20.0). **Only the agent’s own sessions may be closed**: a tab the user opened is refused, so the agent never ends a terminal the user is working in. It also works on an **exited session that is still in read-only retention** — that is its release entry point (by default it is retained **until explicitly closed**, never on a timer) |
|
|
109
117
|
| `tty_stats` | Read live host metrics for a session’s machine (0.20.0): CPU / memory / disk / TCP connections / network rates / temperature / uptime. Local sessions report the host; SSH sessions report that remote host over a separate non-PTY channel that never touches the terminal. Check it before deploying or load-testing |
|
|
110
118
|
| `tty_capture` | Read recent output (last N lines, ANSI stripped by default, `raw:true` for the raw stream); **`last:true` returns only the output + exit code of the previous completed command** (shell integration markers, see the next section); when a command is **in flight** (just sent, completion marker not in yet) it returns `inProgress:true` without the stale result, so the previous command is never mistaken for this one (0.19.0) |
|
|
111
119
|
| `tty_screen` | Read the **currently visible screen** as rendered (xterm-headless virtual screen, plain text) — it can genuinely read TUI interfaces such as vim / htop / menus |
|
|
112
|
-
| `tty_expect` | Wait with a regex for a readiness signal
|
|
113
|
-
| `tty_send` | Send keys/text to a given session (such as `q` to a dev server, or a menu selection) |
|
|
120
|
+
| `tty_expect` | Wait with a regex for a readiness signal (dev server URL, build finished, …). It **looks back first** at output that has not been read yet (including the full output of “the previous command”, so a command that finished instantly no longer burns the whole timeout) and then waits for subsequent output; on a hit it returns `matched:true` plus `matchedFrom` (`live` produced during this wait / `last` the previous command’s output / `buffered` buffered output that had already arrived). A timeout does not throw (`matched:false` + tail output, and it **does not consume the unread region**, so a different pattern can still look back at the same output), and a command that ends early also returns early with its exit code; **the echo of the command just sent never counts as a hit** (via the OSC 133 A..B boundary, or via the send record where there are no markers — see the next section; when only the echo ever matched, the timeout result carries `echoOnly:true`); at most 5 in-flight calls per session, and the accumulated window keeps only the last 64KB (0.19.0; look-back matching, see D72; echo exclusion, see D75) |
|
|
121
|
+
| `tty_send` | Send keys/text to a given session (such as `q` to a dev server, or a menu selection). End a command with `\n`: on a **local Windows session** a trailing bare LF is normalised to CRLF (D74, see the “Windows hosts” section); non-Windows and SSH sessions pass through untouched. On an **exited** session it fails explicitly (such sessions are read-only) |
|
|
114
122
|
| `sftp_list` | List a remote SSH directory (name/type/size/mtime, directories first); `book` is the connection-book entry name and `path` defaults to the login home; at most 500 entries by default (`truncated:true` beyond that), and `isSymlink` distinguishes a symlink from a real directory (0.19.0) |
|
|
115
123
|
| `sftp_read` | Read a remote **text** file (≤256KB by default, adjustable to 1MB, truncated beyond that); `offset` pages from a given byte (handy for log tails), an invalid `maxBytes` errors out instead of silently falling back, and binary detection is a double test (NUL + illegal-UTF-8 ratio) (0.19.0) |
|
|
116
124
|
| `sftp_write` | Write a remote text file (overwrite by default, `append:true` appends; ≤1MB per call) |
|
|
@@ -134,6 +142,39 @@ the orphan collector** (which only reaps disconnected *user* sessions); it ends
|
|
|
134
142
|
`tty_close` or the user closing the tab. Conversely the agent **cannot close a user-opened tab**
|
|
135
143
|
(`tty_close` refuses explicitly).
|
|
136
144
|
|
|
145
|
+
### Read-only retention after the process exits (D77)
|
|
146
|
+
|
|
147
|
+
A process exit **no longer means the session is gone** — the session turns into a **read-only retained** one,
|
|
148
|
+
**kept until it is explicitly closed** by default (`EXITED_RETAIN_MS = ∞`; a host/plugin restart clears them).
|
|
149
|
+
While retained:
|
|
150
|
+
|
|
151
|
+
- **Reads keep working**: `tty_capture` (tail or `last`, including the final batch of output printed
|
|
152
|
+
before exit), `tty_screen` (the screen as of the exit) and `tty_list` (entry carries `exited:true` +
|
|
153
|
+
`exitCode`/`signal`) all still answer, and their results carry an `exited` marker;
|
|
154
|
+
`tty_expect` no longer burns its timeout — it settles against the existing output and returns
|
|
155
|
+
`exited:true`;
|
|
156
|
+
- **Writes are refused**: `tty_send` reports an explicit error (the process is gone; a write would
|
|
157
|
+
vanish into a dead PTY), and `tty_resize` / `tty_stats` no longer apply to it (stats honestly returns
|
|
158
|
+
`available:false`);
|
|
159
|
+
- **No re-attaching a terminal**: `attach` is explicitly refused (there is no live PTY; the panel does
|
|
160
|
+
not create a tab for it either) — read the output with `tty_capture`;
|
|
161
|
+
- **Who releases it**: the agent’s `tty_close` (its own sessions only), the user closing that tab in the
|
|
162
|
+
panel, or a host/plugin restart (retention lives in memory). The concurrency limit counts **live
|
|
163
|
+
sessions only** (retained ones do not consume a slot), and at most `MAX_EXITED_SESSIONS` (8) retained
|
|
164
|
+
sessions are kept — the oldest is evicted beyond that (each eviction logs the sid it drops). So the
|
|
165
|
+
rule is “the last 16 exited sessions stay readable”, which is the only bound on memory and handles
|
|
166
|
+
(16 of them is on the order of ~20MB).
|
|
167
|
+
|
|
168
|
+
Why **not** a timer: memory and handles are not actually time-driven (see above), while a deadline is a
|
|
169
|
+
second surprise for whoever reads the output — the gap between “the command finished” and “the next
|
|
170
|
+
round reads the result” spans people walking away and queued model turns. An output that vanishes on
|
|
171
|
+
schedule is harder to reason about than one you have to close. If you do want a time bound (or no
|
|
172
|
+
retention at all), set `EXITED_RETAIN_MS` to a millisecond value / 0 — the `reapExited` path is still
|
|
173
|
+
wired into the reaper, and `tty_list`’s `retainMs` field only appears when a timer is in play.
|
|
174
|
+
|
|
175
|
+
This is why “`tty_open` a command that finishes and read the result afterwards” now just works — no
|
|
176
|
+
`sh` wrapper needed (the request in issue #4).
|
|
177
|
+
|
|
137
178
|
### Shell integration (OSC 133/7, 0.4.0)
|
|
138
179
|
|
|
139
180
|
At spawn time hooks are injected through the existing `-c` wrapper layer according to the shell type (transparent to the user, no rc changes):
|
|
@@ -152,6 +193,37 @@ At spawn time hooks are injected through the existing `-c` wrapper layer accordi
|
|
|
152
193
|
code / `OSC 7 file://…` cwd reporting (`tty_list.cwd` follows `cd`, and SSH sessions report the remote path);
|
|
153
194
|
- Other shells are silently disabled; `shellIntegration: false` turns the whole thing off (escape hatch).
|
|
154
195
|
|
|
196
|
+
**Look-back matching and the read watermark (D72)**: `tty_expect` first looks back at output that has
|
|
197
|
+
**not been read yet** (when a command finishes instantly, the marker you are waiting for is already in the
|
|
198
|
+
buffer). The look-back lower bound is raised to “just after the last `B` marker” inside the unread region —
|
|
199
|
+
the echo sits between `A` and `B`, so text in the echo never counts as a hit (deliberately no text
|
|
200
|
+
comparison here: line wrapping / ANSI / multi-line paste would break it).
|
|
201
|
+
|
|
202
|
+
- **Environments without `B` markers** (integration off / local Windows tabs / a remote host without the
|
|
203
|
+
hooks) have no boundary to cut at, so since D75 the echo is excluded by a different test: the command
|
|
204
|
+
lines that the most recent `tty_send` submitted are stripped out of the candidate text and the pattern
|
|
205
|
+
is retried — only “no longer matches after stripping” counts as a pure echo. That removes the false
|
|
206
|
+
positive where the command never ran yet the wait reported `matched:true`, while the same text really
|
|
207
|
+
printed later still matches. When the wait times out and the pattern only ever matched the echo, the
|
|
208
|
+
result carries `echoOnly:true` and the message says plainly that the command was probably never
|
|
209
|
+
executed. **Boundary**: only lines written by `tty_send` are known (typing in the panel is not
|
|
210
|
+
simulated as line editing), and stripping is disabled while a command is running (in a full-screen TUI
|
|
211
|
+
redraw, the typed text appearing on screen is real output);
|
|
212
|
+
- **The tradeoff in that look-back range**: raising the lower bound to “just after the last `B` marker”
|
|
213
|
+
costs you the unread output that precedes the newest command’s start. It deliberately prefers a miss
|
|
214
|
+
over a false hit — treating the echo as a match would make the agent believe the event just happened.
|
|
215
|
+
Use `tty_capture` for that text. The mirror-image corner exists too: right after `tty_send`, before the
|
|
216
|
+
new command’s `B` marker arrives, the unread region still holds the previous command’s output, so a
|
|
217
|
+
pattern in it matches at once (the agent’s next call almost always lands after that `B`, where this
|
|
218
|
+
text has already been cut off by it);
|
|
219
|
+
- **Read watermark**: whatever `tty_capture` / `tty_expect` returns counts as read, and later
|
|
220
|
+
`tty_expect` calls will not look back at it (use `tty_capture` to re-read older output). `tty_screen`
|
|
221
|
+
deliberately does **not** advance it (the screen is a different representation, not a text stream);
|
|
222
|
+
`tty_send` only initialises it (the output of the command just sent has not been seen yet); **a timeout
|
|
223
|
+
does not consume the unread region** either (another pattern can still look back at the same output).
|
|
224
|
+
A tab the user opened gets its watermark placed at the moment the agent first touches it — history
|
|
225
|
+
before that does not count as unread.
|
|
226
|
+
|
|
155
227
|
SSH sessions are scheduled on the same table: entries with `kind: 'ssh'` in `tty_list` are identified by
|
|
156
228
|
`target` (user@host[:port]), and `tty_capture` / `tty_expect` / `tty_send` are used exactly as for local
|
|
157
229
|
sessions — dev server logs and key interactions on the remote machine remain available as usual.
|
|
@@ -460,7 +532,22 @@ and the agent tools all reuse the same scheduling.
|
|
|
460
532
|
`agentForward` and show `· fwd` in the list;
|
|
461
533
|
- **`~/.ssh/config` import (0.4.0)**: “Import from ~/.ssh/config” in the connection-book area of the settings
|
|
462
534
|
card — parses `HostName/User/Port/IdentityFile` into candidate entries (skipping wildcard blocks and
|
|
463
|
-
entries without a User; `Include` is not expanded), skips same names, and writes them on “save
|
|
535
|
+
entries without a User; `Include` is not expanded), skips same names, and writes them on “save”.
|
|
536
|
+
**Every skip is now reported** (this round): blocks that depend on a jump host (`ProxyJump` /
|
|
537
|
+
`ProxyCommand`) are **not imported and are named** — this version does not support jump hosts, and
|
|
538
|
+
importing one would only produce an entry that cannot connect and reports a generic timeout; the
|
|
539
|
+
over-limit count and the “not a concrete host” count are reported too. `ProxyJump none` /
|
|
540
|
+
`ProxyCommand none` mean an explicit direct connection and are imported as usual.
|
|
541
|
+
**`ProxyJump` is resolved into the entry's jump host (single hop) and imported with it**: the
|
|
542
|
+
value may be `user@host:port` (IPv6 as `[::1]:22`) or another concrete Host in the same config
|
|
543
|
+
(resolved by block name, in any order); the jump host **inherits the target hop's credentials**
|
|
544
|
+
by default, so set `jump.keyPath` / `jump.password` on the entry when it needs its own.
|
|
545
|
+
Unresolvable ones (missing alias, or an alias that itself needs a jump) are still skipped and
|
|
546
|
+
named; `ProxyCommand` is not supported (it would let a settings field drive arbitrary local
|
|
547
|
+
command execution — a different trust tier, see project ROADMAP item 2). **The dialog also has a “Jump host” section**: one
|
|
548
|
+
`[user@]host[:port]` input (matching OpenSSH), with user / auth / key / passphrase overrides
|
|
549
|
+
revealed only after ticking “use separate credentials”; entry rows show “⇢ via X”, and the
|
|
550
|
+
“Test” button reports the jump hop on its own line;
|
|
464
551
|
- **Credential-reference picker (0.4.0; decoupled from the env plugin since 0.17)**: next to the password /
|
|
465
552
|
passphrase fields in the SSH dialog there is a filter box plus a height-limited list whose candidates are
|
|
466
553
|
**the reference names the credential store already knows** (the host reads the `refs:` keys of
|
|
@@ -633,13 +720,23 @@ ctx.inject(['ttyTerminal'], (c) => {
|
|
|
633
720
|
|
|
634
721
|
- Command tabs are **not persisted in tmux** (commands are short-lived and attaching is meaningless) and do
|
|
635
722
|
not go through a login shell; the SSH side uses `conn.exec(command, {pty})`, and the local side uses
|
|
636
|
-
`sh -c 'export TERM=…; exec <command>'
|
|
723
|
+
`sh -c 'export TERM=…; exec <the same shell> -c <command>'` — the outer `exec` makes the process
|
|
724
|
+
the command itself (faithful exit code and signal), and the **inner `-c`** is what makes the whole
|
|
725
|
+
string run as shell code: `cd x && cmd`, `a; b`, `for …; do …; done` all run to completion
|
|
726
|
+
(D78: the old form appended the command straight after `exec`, so only the first one ran and a
|
|
727
|
+
builtin such as `cd` ended the wrapper shell outright).
|
|
637
728
|
- **Command tabs reopen automatically**: after a host restart / reconnect the sid is gone, and the client
|
|
638
729
|
re-runs the command from the original spec for tabs with `spawnSpec.command` (ordinary non-persistent tabs
|
|
639
730
|
keep the old “click to retry” behavior). After a page refresh they are restored by the original command too.
|
|
640
731
|
- The command comes from a **host-side plugin** (not from remote user input), so its trust level equals the
|
|
641
732
|
plugin’s own; tty only validates the shape: non-empty, single line, length ≤2000 (a newline would break the
|
|
642
733
|
local `-c` wrapper layer).
|
|
734
|
+
- **“Single line, ≤2000” is this channel’s rule, not `tty_open`’s** (raised in the 2026-09-27 review): the
|
|
735
|
+
`command` in a `spawn` / `ssh` frame gets spliced into the `-c` wrapper, so `sanitizeCommand` rejects
|
|
736
|
+
newlines and NUL. The agent’s `tty_open.command` takes a different path — the command is wrapped **whole**
|
|
737
|
+
in `shSingleQuote` and handed to the inner `-c` (see above), so it **may contain newlines** (multi-line
|
|
738
|
+
scripts do run) and is bounded only by the tool parameter’s string type. Do not read this section’s limit
|
|
739
|
+
as a `tty_open` restriction.
|
|
643
740
|
- The service name `ttyTerminal` is likewise not declared on the `Context` type surface; inject it as an
|
|
644
741
|
optional dependency; when tty is not installed or is older than 0.14.0 it never fires (dsh-docker degrades
|
|
645
742
|
to “copy command”).
|
|
@@ -777,7 +874,7 @@ ctx.inject(['ttyPanel'], (c) => {
|
|
|
777
874
|
| C→S | `{t:'attach', sid}` | Reattach an orphan session (inside the keep-alive window): after `ready(reattached:true)` a single `data` frame replays the output buffer |
|
|
778
875
|
| C→S | `{t:'statsOn' \| 'statsOff', sid}` | Subscribe/unsubscribe that session’s server status bar (0.17.0): driven by tab visibility, and the host collects only while a subscription exists (lazy start + unsubscribing stops the meter and closes the remote channel) |
|
|
779
876
|
| S→C | `{t:'ready', sid, pid, kind, target?, persist?, reattached?}` | Session ready; `kind:'local'` carries a pid, while `kind:'ssh'` has pid=null and target=user@host[:port]; attach reuses this frame with `reattached:true`; `persist:true` means a tmux persistent session (0.10.0) |
|
|
780
|
-
| S→C | `{t:'data', sid, d}` | Terminal output (utf8 text, StringDecoder covers multi-byte sequences split across frames); **coalesced into frames over a 12ms window / 64KB threshold** (0.4.1), with a forced flush before exit/kill to guarantee frame order |
|
|
877
|
+
| S→C | `{t:'data', sid, d}` | Terminal output (utf8 text, StringDecoder covers multi-byte sequences split across frames); **coalesced into frames over a 12ms window / 64KB threshold** (0.4.1), with a forced flush before exit/kill to guarantee frame order — **the last batch before exit is always sent** (D76: it used to be swallowed whole by the exit path’s own `closed` guard, so a process that printed and exited immediately was permanently missing its final lines) |
|
|
781
878
|
| S→C | `{t:'stats', sid, stats}` | Resource metric frame (0.17.0): `{cpuPct, cores, memUsed, memTotal, memPct, diskUsed, diskTotal, diskPct, uptimeSec, tcpConns, rxRate, txRate, tempC?}`; missing fields are omitted (best-effort, the frontend shows “n/a”), byte fields are bytes and rates are B/s |
|
|
782
879
|
| S→C | `{t:'exit', sid, code, signal}` | The PTY exit fact (exactly once; after attach moves to a new connection it is still delivered over the current connection) |
|
|
783
880
|
| S→C | `{t:'error', sid?, m}` | Error |
|
|
@@ -991,7 +1088,13 @@ Host half (src/index.ts)
|
|
|
991
1088
|
│ /api/dsh-tty/credential-refs (reference names known to the credential store — names only, see
|
|
992
1089
|
│ “Credential storage”), /api/dsh-tty/env-vars (variable names managed by the env plugin), /api/dsh-tty/known-hosts
|
|
993
1090
|
│ (TOFU fingerprint prefill, src/known-hosts.ts parses hashed entries too),
|
|
994
|
-
│ /api/dsh-tty/shells (shell path candidates) — all behind the loopback fence
|
|
1091
|
+
│ /api/dsh-tty/shells (shell path candidates) — all behind the loopback fence (hardened
|
|
1092
|
+
│ tier: the whole 127/8 range + DNS confirmation for alias hostnames + source checks before
|
|
1093
|
+
│ DNS; one implementation only, in @hyzyn/dsh-kit); **state-changing endpoints additionally
|
|
1094
|
+
│ require a same-origin proof** (POST /probe, sftp mkdir/rename/remove/upload, local-fs
|
|
1095
|
+
│ mkdir/rename/remove/transfer); read-only endpoints (including sftp /list and /download)
|
|
1096
|
+
│ need no proof, so bare curl still works. POST /config is deliberately exempt — it is the
|
|
1097
|
+
│ only recovery entry once the plugin is disabled
|
|
995
1098
|
├─ SFTP (src/sftp.ts, 0.7.0): lazy connection pool (reclaimed after 120s idle, reconnected on
|
|
996
1099
|
│ demand when dropped, TOFU shared) → POST /api/dsh-tty/sftp/list|mkdir|rename|remove|download|
|
|
997
1100
|
│ upload (spec in body/headers, credentials never in the URL; uploads/downloads streamed via pipe) +
|
package/README.md
CHANGED
|
@@ -75,6 +75,10 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
75
75
|
没有意义,也不再注入;
|
|
76
76
|
- **「跑一条命令」的标签**(docker exec、agent 起的命令标签)走 `cmd /c` 或
|
|
77
77
|
`PowerShell -Command`;
|
|
78
|
+
- **输入行尾归一化(D74,2026-09-27 真机报告)**:conhost 的 Enter 是 **CR**,裸 LF 只把光标
|
|
79
|
+
下移一格、**不提交命令行**——`tty_send` 按工具描述发 `echo X\n` 时命令停在输入行上(没有输出、
|
|
80
|
+
没有新提示符,看起来像「发出去了但没执行」)。**win32 的本地会话**上插件现在把裸 LF 补成 CRLF
|
|
81
|
+
(已经是 CRLF 的不重复补;非 Windows 与 SSH 会话原样透传),所以照描述写 `\n` 这条主路径直接可用;
|
|
78
82
|
- **不支持的三项(宿主侧自动关掉,设置卡片里写明原因)**:
|
|
79
83
|
- **shell 集成(OSC 133/7)**:注入靠 POSIX rc 桩 + `-c` 包装层,cmd / PowerShell 上都不成立,
|
|
80
84
|
因此恒关 —— **本地标签**的 `tty_list.cwd` 跟随 `cd`、`tty_capture{last}` 与 `tty_expect`
|
|
@@ -87,7 +91,9 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
87
91
|
内容标记一致;不写死字节数——每次重建都会变)、`[dsh-tty] mounted (shell=C:\WINDOWS\system32\cmd.exe)`;x64 Windows 由 CI
|
|
88
92
|
的三平台矩阵覆盖(build / typecheck / test,见「开发」)。
|
|
89
93
|
- **端到端覆盖(0.19.0)**:`scripts/windows-smoke.mjs` 在 CI 的 windows-latest 上跑真实 `cmd.exe`
|
|
90
|
-
的 spawn → 输入回显 → kill →
|
|
94
|
+
的 spawn → 输入回显 → kill → 重开,外加两条只在真机上成立的链路(W6:`tty_send` 的裸 LF 经归一化
|
|
95
|
+
后 conhost 真的执行了命令,D74;W7:`command` 型会话跑完退出后转只读保留、`tty_capture` 仍读得到
|
|
96
|
+
退出前的输出,D77)。它抓出并钉住了「强杀本地 PTY 会崩宿主」这类只在 Windows
|
|
91
97
|
出现的问题——node-pty 在 Windows 上不接受 signal,而它的 `_deferNoArgs` 会把这个异常推迟到
|
|
92
98
|
socket 回调里抛出,调用方的 try/catch 拦不住。
|
|
93
99
|
|
|
@@ -97,14 +103,14 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
97
103
|
|
|
98
104
|
| 工具 | 作用 |
|
|
99
105
|
| --- | --- |
|
|
100
|
-
| `tty_list` |
|
|
101
|
-
| `tty_open` | **自己开一个终端会话**(0.20.0):本地 shell,或 `command`
|
|
102
|
-
| `tty_close` | 关掉一个由 `tty_open` 开的会话(0.20.0)。**只允许关 agent 自己开的**:用户在面板里开的标签会被拒绝——agent
|
|
106
|
+
| `tty_list` | 列出终端会话(sid / kind(local\|ssh)/ target / pid / **cwd 实时跟随 cd** / 活动时间;tmux 持久会话带 `persist` 标记;agent 自己开的带 `owner: 'agent'`)。**含进程已退出但仍在只读保留期内的会话**(`exited:true` + 退出码/信号,见下节) |
|
|
107
|
+
| `tty_open` | **自己开一个终端会话**(0.20.0):本地 shell,或 `command` 直接跑一条命令(dev server / watch;**按整段 shell 代码执行**——`cd x && cmd`、`a; b`、多行脚本都可以,且不受下面「命令标签单行 ≤2000」那条客户端约束,D78),`persistName` 可要 tmux 持久化。**开出来的会话出现在用户的终端面板里**(普通标签、用户可见可接管),不做隐形会话 |
|
|
108
|
+
| `tty_close` | 关掉一个由 `tty_open` 开的会话(0.20.0)。**只允许关 agent 自己开的**:用户在面板里开的标签会被拒绝——agent 不越权结束用户正在用的终端。对**已退出但仍只读保留着**的会话同样可用——那是它的释放入口(默认**保留到显式关闭**,不按时间释放) |
|
|
103
109
|
| `tty_stats` | 读会话所在机器的实时指标(0.20.0):CPU / 内存 / 磁盘 / TCP 连接数 / 网速 / 温度 / 在线时长。本地会话取宿主机;SSH 会话取那台远程主机(另开一段非 PTY 通道,不影响终端)。部署、压测前先看它 |
|
|
104
110
|
| `tty_capture` | 读取近期输出(尾部 N 行,默认清洗 ANSI,`raw:true` 取原始流);**`last:true` 只返回上一条已完成命令的输出 + 退出码**(shell 集成标记,见下节);命令**在途**时(刚发送、完成标记未到)返回 `inProgress:true` 且不带旧结果——避免把上一条的输出当成这一条(0.19.0) |
|
|
105
111
|
| `tty_screen` | 读取**当前可见屏幕**的渲染结果(xterm-headless 虚拟屏,纯文本)——能真正读懂 vim / htop / 菜单等 TUI 界面 |
|
|
106
|
-
| `tty_expect` |
|
|
107
|
-
| `tty_send` | 向指定会话发送按键/文本(如 dev server 的 q
|
|
112
|
+
| `tty_expect` | 用正则等一个就绪信号(dev server URL、构建完成等)。**先回溯**还没被读过的输出(含「上一条命令」的完整输出,故命令瞬间跑完也不会白等),再等后续输出;命中即返回 `matched:true` + `matchedFrom`(`live` 本次等待期间新产生 / `last` 上一条命令的输出 / `buffered` 此前已到达的缓冲输出)。超时不抛错(`matched:false` + 尾部输出,且**不消耗未读区**——换个 pattern 还能回溯到同一段),命令提前结束也会带退出码早停;**刚发进去的命令回显不算命中**(靠 OSC 133 的 A..B 边界,无标记环境靠发送记录剔除,见下节;只命中回显时超时结果带 `echoOnly:true`);同一会话在途调用最多 5 个,累积窗口只保留尾部 64KB(0.19.0;回溯匹配见 D72,回显剔除见 D75) |
|
|
113
|
+
| `tty_send` | 向指定会话发送按键/文本(如 dev server 的 q 键、菜单选择)。命令以 `\n` 结尾即可:**Windows 本地会话**上行尾裸 LF 会被归一成 CRLF(D74,见「Windows 宿主」一节),非 Windows 与 SSH 会话原样透传。对**已退出**的会话会明确报错(那种会话只能读) |
|
|
108
114
|
| `sftp_list` | 列出 SSH 远程目录内容(名称/类型/大小/修改时间,目录在前);`book` 为连接簿条目名,`path` 缺省为登录 home;默认最多 500 项(超限 `truncated:true`),`isSymlink` 区分软链与真目录(0.19.0) |
|
|
109
115
|
| `sftp_read` | 读取远程**文本**文件(默认 ≤256KB 可调至 1MB,超出截断);`offset` 可从指定字节分页(适合读日志尾部),非法 `maxBytes` 直接报错,二进制判定 = NUL + 非法 UTF-8 占比双判据(0.19.0) |
|
|
110
116
|
| `sftp_write` | 写远程文本文件(默认覆盖,`append:true` 追加;单次 ≤1MB) |
|
|
@@ -125,6 +131,33 @@ dsh plugin --profile web add link:$(pwd)/packages/tty # 仓库开发调试
|
|
|
125
131
|
(回收器只收「断连的用户会话」),关闭入口是 agent 的 `tty_close` 或用户在面板里关标签;
|
|
126
132
|
反过来说,agent 也**关不掉用户开的标签**(`tty_close` 会明确拒绝)。
|
|
127
133
|
|
|
134
|
+
### 进程退出后的「只读保留」(D77)
|
|
135
|
+
|
|
136
|
+
会话的进程退出**不再等于会话消失**——它转成**只读保留态**,
|
|
137
|
+
**默认保留到显式关闭**(`EXITED_RETAIN_MS = ∞`;宿主 / 插件重启即清空),期间:
|
|
138
|
+
|
|
139
|
+
- **读得到**:`tty_capture`(尾部/`last`,含退出前的最后一批输出)、`tty_screen`(退出那一刻
|
|
140
|
+
的屏)、`tty_list`(条目带 `exited:true` + `exitCode`/`signal`)都照常可用,
|
|
141
|
+
结果里也带 `exited` 标记;`tty_expect` 不再空等——它按现存输出立刻结算并带 `exited:true`;
|
|
142
|
+
- **写不进去**:`tty_send` 明确报错(进程已经没了,写进去只会在死 PTY 上静默消失),
|
|
143
|
+
`tty_resize` / `tty_stats` 同样不再作用于它(stats 如实回 `available:false`);
|
|
144
|
+
- **不再接回终端**:`attach` 会被明确拒绝(没有活着的 PTY;面板也不会为它建标签),
|
|
145
|
+
输出用 `tty_capture` 读;
|
|
146
|
+
- **谁释放它**:agent 的 `tty_close`(仅限自己开的)、用户在面板里关那个标签、
|
|
147
|
+
或宿主 / 插件重启(保留是内存态)。并发上限**只数活着的会话**(保留态不占名额);
|
|
148
|
+
保留态本身最多留 `MAX_EXITED_SESSIONS`(16)条,超出按最旧淘汰(淘汰时
|
|
149
|
+
`logger.warn` 记下被挤掉的 sid)——也就是「最近的 16 条退出会话一直可读」,
|
|
150
|
+
这是内存与句柄的唯一上界(16 条约 ~20MB 量级)。
|
|
151
|
+
|
|
152
|
+
为什么**不用时间**淘汰:内存与句柄本来就不由时间决定(见上一句),而时间上界对
|
|
153
|
+
使用者是第二重惊喜——「命令跑完 → 下一轮读结果」之间隔着人离开、模型排队,多久
|
|
154
|
+
都有可能,一个到期就消失的输出比「要主动关掉」更难理解。想要时间上界(或干脆
|
|
155
|
+
不保留)把 `EXITED_RETAIN_MS` 改成毫秒数 / 0 即可:`reapExited` 那条通路还在,
|
|
156
|
+
回收器每轮都会调它;`tty_list` 的 `retainMs` 字段也只在按时间释放时才出现。
|
|
157
|
+
|
|
158
|
+
这就是「`tty_open` 跑一条会结束的命令、回头再取结果」能直接用的原因——不需要套一层
|
|
159
|
+
`sh`(issue #4 的诉求)。
|
|
160
|
+
|
|
128
161
|
### shell 集成(OSC 133/7,0.4.0)
|
|
129
162
|
|
|
130
163
|
spawn 时经既有的 `-c` 包装层按 shell 类型注入钩子(对用户透明,不改 rc):
|
|
@@ -144,6 +177,29 @@ spawn 时经既有的 `-c` 包装层按 shell 类型注入钩子(对用户透
|
|
|
144
177
|
SSH 会话则上报远程路径);
|
|
145
178
|
- 其他 shell 静默关闭;配置 `shellIntegration: false` 可整体关掉(逃生门)。
|
|
146
179
|
|
|
180
|
+
**回溯匹配与已读水位线(D72)**:`tty_expect` 会先回看**还没被读过**的输出(命令瞬间跑完时
|
|
181
|
+
要等的标记早就在缓冲区里了),回看的起点抬到「未读区内最后一个 `B` 标记之后」——回显落在 `A`
|
|
182
|
+
与 `B` 之间,所以回显里的字样不会被当成命中(这里刻意不做文本比对:折行 / ANSI / 多行粘贴
|
|
183
|
+
都会把它打碎)。
|
|
184
|
+
|
|
185
|
+
- **没有 `B` 标记的环境**(未开集成 / Windows 本地标签 / 远端未装集成)没有边界可切,D75 起
|
|
186
|
+
改用另一条判据剔除回显:把**最近 `tty_send` 提交过的命令行**从候选文本里削掉再试一次,
|
|
187
|
+
只有「削掉后不再命中」才算纯回显——这样「命令没执行、等待却自称 matched」这条假阳性没了,
|
|
188
|
+
而回显之后真的跑出来的同一段文本照旧命中。等满超时且 pattern 只命中过回显时,结果带
|
|
189
|
+
`echoOnly:true`,文案会直说「命令大概率没有被执行」。**边界**:只认 `tty_send` 写进去的行
|
|
190
|
+
(用户在面板里逐键敲的不做行编辑模拟),且命令正在跑(TUI 全屏重画里出现输入文本是真实
|
|
191
|
+
输出)时不启用剔除;
|
|
192
|
+
- **回看范围的取舍**:起点抬到「未读区内最后一个 `B` 标记之后」,代价是最后一条命令**开始
|
|
193
|
+
之前**的那段未读输出不在回看范围内。刻意选「宁可漏、不可误报」——把回显当成命中会让 AI
|
|
194
|
+
以为事件刚刚发生。要那段内容用 `tty_capture`。反过来的窄窗口也有一个:`tty_send` 之后、
|
|
195
|
+
新命令的 `B` 标记到达之前,未读区里仍然是上一条命令的输出,里面有要等的 pattern 就会立即
|
|
196
|
+
命中(AI 的下一次调用几乎总在新 `B` 之后,那种情况下这段已被新 `B` 切掉);
|
|
197
|
+
- **已读水位线**:`tty_capture` / `tty_expect` 返回的内容算「已读」,之后的 `tty_expect` 不再
|
|
198
|
+
回扫它们(要回看更早的内容用 `tty_capture`)。`tty_screen` 刻意**不**推进(屏幕是另一种
|
|
199
|
+
表示,不是文本流);`tty_send` 只初始化不推进(刚发出去的命令输出还没被看见);**超时也不
|
|
200
|
+
消耗未读区**(换一个 pattern 还能回溯到同一段)。用户自己开的标签在 agent 第一次触达时
|
|
201
|
+
才把水位线落在当下——那之前的历史输出不算「未读」。
|
|
202
|
+
|
|
147
203
|
SSH 会话同表调度:`tty_list` 里 `kind: 'ssh'` 的条目按 `target`
|
|
148
204
|
(user@host[:port])识别,`tty_capture` / `tty_expect` / `tty_send` 用法与
|
|
149
205
|
本地会话完全一致——远程机器上的 dev server 日志与按键交互照常可用。
|
|
@@ -409,7 +465,44 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
409
465
|
失效。连接簿条目随 `agentForward` 保存,列表里显示 `· fwd`;
|
|
410
466
|
- **`~/.ssh/config` 导入(0.4.0)**:设置卡片连接簿区「从 ~/.ssh/config
|
|
411
467
|
导入」——解析 `HostName/User/Port/IdentityFile` 生成候选条目(跳过通配符
|
|
412
|
-
块与无 User 条目,`Include`
|
|
468
|
+
块与无 User 条目,`Include` 不展开),同名跳过,随「保存」写入。
|
|
469
|
+
**每一种跳过都会当场说明**(本轮起):依赖跳板机的块(`ProxyJump` / `ProxyCommand`)
|
|
470
|
+
**不导入并点名**——本版本不支持跳板机,导进来只会得到一条连不上、且只报通用超时的条目;
|
|
471
|
+
超过导入上限的条数与「非具体主机」的条数也各自报数。`ProxyJump none` /
|
|
472
|
+
`ProxyCommand none` 是显式直连,照常导入。
|
|
473
|
+
**`ProxyJump` 会解析成连接条目的跳板机(单跳)一起导入**:值可以是 `user@host:port`
|
|
474
|
+
(IPv6 写 `[::1]:22`),也可以是同一份 config 里的另一个具体 Host(按块名解析,块序任意);
|
|
475
|
+
跳板机缺省**继承目标那一跳的凭据**,要不同的钥匙就在条目里填 `jump` 的 `keyPath`/`password`。
|
|
476
|
+
解析不出来的(别名缺失、别名自己又依赖跳板机)仍会跳过并点名。
|
|
477
|
+
**`ProxyCommand` 永不自动导入**(单独报数并说明为什么):它等于让设置字段驱动本地任意
|
|
478
|
+
命令执行,所以只在**手动填写 + 打开开关**之后才会跑(见下一段)。
|
|
479
|
+
**对话框里也有「跳板机」一段**:一个 `[用户@]主机[:端口]` 输入框(与 OpenSSH 写法一致),
|
|
480
|
+
勾「使用独立凭据」才展开用户名 / 认证方式 / 私钥 / 口令;条目行显示「⇢ 经 X」,「试连」
|
|
481
|
+
会把跳板机那一跳单列出来;
|
|
482
|
+
- **代理命令 / ProxyCommand(0.21.0,默认关)**:连接簿条目(或连接对话框)里可填一条
|
|
483
|
+
**本机命令**,它的 stdin/stdout 就是到目标的 SSH 传输——等价于 OpenSSH 的 `ProxyCommand`
|
|
484
|
+
(典型写法 `ssh -W %h:%p bastion`)。支持 `%h` 目标主机、`%p` 端口、`%r` 用户名、`%n` 主机、
|
|
485
|
+
`%%` 字面 `%`,其余 `%X` 原样保留;**代入值只允许字母数字与 `._@:[]-`,含 shell 特殊字符
|
|
486
|
+
直接拒绝执行**(不做「按平台各写一套转义」:命令最终交给 `sh -c` / `cmd /c`,两边规则不同)。
|
|
487
|
+
**闸门**:设置卡片「允许 ProxyCommand(本机执行命令)」**默认关**,它的**提权有两条通道**:
|
|
488
|
+
① **启动环境变量** `DSH_TTY_ALLOW_PROXY_COMMAND=1`(最严档,进程启动时采样,只认继承来的
|
|
489
|
+
`process` 层——写项目 `.env` 或 `~/.dsh/env.yml` **不算**授权);② **就地提权**(免重启):
|
|
490
|
+
卡片上点那个开关 → 面板给出一条「在宿主终端执行」的命令 → 执行后十秒内解锁并替你打开开关。
|
|
491
|
+
HTTP / 界面**不能凭空打开**它(给 `true` 而无授权会被 400 拒绝并说清两条路),但**关掉**永远
|
|
492
|
+
可用(紧急刹车不能依赖重启)。授权是**持久**的(落在 `<DSH home>/dsh-kit/capability-grants.json`,
|
|
493
|
+
0600):宿主重启后直接生效、不再确认,所以每次启动会打一行
|
|
494
|
+
`[dsh-tty] elevation: load capability=… via=file grantedAt=…`,卡片上也显示「已授权 · 时刻」并给
|
|
495
|
+
「撤销宿主授权」入口。**升级影响**:升级前靠界面打开的这个开关会变成关。为什么这么设计:
|
|
496
|
+
回环围栏与同源证明都拦不住跨站页面与页内脚本,而这一档是「配置里写一行就在本机跑命令」——
|
|
497
|
+
配置界面若能凭空提权,这道闸门等于没有(细节见
|
|
498
|
+
[architecture.md § 7](../../docs/architecture.md#7-一条请求经过什么))。关着时携带代理命令的连接
|
|
499
|
+
**明确失败、不退回直连**(直连多半也连不上,还会把配置问题伪装成网络问题),「试连」在
|
|
500
|
+
阶段 0 就返回并点名这个开关。与跳板机同时填时按 OpenSSH 语义**跳板机优先**,并记一条 warn。
|
|
501
|
+
闸门只有一处(本插件 settings):docker 容器面板读同一个开关——连接簿配一次、两处生效;
|
|
502
|
+
代价是代理命令是**本机**执行的,docker 目标也会用这条命令(详见
|
|
503
|
+
[docs/proxyjump-plan.md](../../docs/proxyjump-plan.md) §6.1 的闸门定案表);
|
|
504
|
+
与跳板机一样,**它是另一条独立传输,收尾必须成对**(终端 / SFTP / 隧道 / 探针四条路都在
|
|
505
|
+
同一个收尾入口里 `dispose()`:关传输 + 杀进程组,子进程不会变成常驻孤儿);
|
|
413
506
|
- **凭据引用选择器(0.4.0,0.17 起与 env 插件解耦)**:SSH 对话框的密码/口令字段旁有筛选框 +
|
|
414
507
|
限高列表,候选 = **凭据存储里已有的引用名**(宿主读 `.credentials.yaml` 的 `refs:` 键,
|
|
415
508
|
**只回名字、绝不含值**)∪ **本机连接簿里已经在用的引用名**;点击即填 `env:NAME`,也可手输
|
|
@@ -510,6 +603,7 @@ tmux server(专用 socket `dsh-tty`,与用户自己的 tmux 完全隔离)
|
|
|
510
603
|
| `persistence` | `off` | 会话持久化:`off` 会话随宿主生死(默认);`tmux` 开启后**所有新开的标签默认由 tmux server 托管**、可跨宿主重启恢复(需本机/远程安装 tmux);SSH 对话框可对单次连接取消 |
|
|
511
604
|
| `endOnPageClose` | `false` | 页面(最后一个连接)断开且保活期结束时,是否连 tmux 持久会话一起结束。默认 `false` = 留存可恢复;`true` = 页面关了就不保活(保活期内刷新仍可无缝接回) |
|
|
512
605
|
| `sftpLimits` | `{maxDownloadMb: 1024, maxUploadMb: 2048, maxUploadFiles: 1000}` | SFTP 传输限制(浏览器侧保护,均为 **0 = 不限**):`maxDownloadMb` 单文件下载上限(超限中止并提示用双栏 `⇦`/终端 scp)、`maxUploadMb` 单文件上传上限、`maxUploadFiles` 一次批量/拖拽上传的文件数上限;大文件请走双栏 `⇨/⇦` 服务端直传(字节不经过浏览器,不占内存) |
|
|
606
|
+
| `allowProxyCommand` | `false` | 允许连接簿条目里的**代理命令(ProxyCommand)在本机执行**。默认关:关着时携带代理命令的连接**明确失败**(不退回直连),`~/.ssh/config` 导入**永不**自动带入该字段。docker 容器面板读同一个开关(连接簿只有一处)。支持 `%h/%p/%r/%n/%%` 展开,代入值走白名单;命令的子进程不会残留(四条连接路径共用同一收尾)。**两条提权通道**:① 启动环境变量 `DSH_TTY_ALLOW_PROXY_COMMAND=1`(判定源是宿主的**启动环境快照**,只认继承来的 `process` 层:写项目 `.env` 或 `~/.dsh/env.yml` **不算**授权);② **就地提权**(免重启):设置卡片点那个开关 → 面板给出「在宿主终端执行」的命令 → 十秒内解锁。HTTP / 界面**不能凭空打开**它(给 `true` 而无授权 400 并说清两条路),但**关掉**永远可用;配置里的 `true` 不算授权。授权持久(`<DSH home>/dsh-kit/capability-grants.json`),重启后直接生效并在卡片上显示时刻与「撤销宿主授权」(见 [architecture.md § 7](../../docs/architecture.md#7-一条请求经过什么)) |
|
|
513
607
|
| `statsEnabled` | true | 服务器状态条(0.17.0):按标签可见性采集并推送 CPU / 内存 / 磁盘 / 在线时长 / TCP 连接数 / 网速 / CPU 温度;关闭即停表(远端 exec channel 一并关闭),保存即热生效 |
|
|
514
608
|
|
|
515
609
|
## 连接栏扩展点(客户端服务 `ttyConnbar`,0.13.0)
|
|
@@ -575,12 +669,21 @@ ctx.inject(['ttyTerminal'], (c) => {
|
|
|
575
669
|
```
|
|
576
670
|
|
|
577
671
|
- 命令标签**不做 tmux 持久化**(命令短命,attach 无意义),也不走登录 shell;
|
|
578
|
-
SSH 侧用 `conn.exec(command, {pty})`,本地侧用
|
|
672
|
+
SSH 侧用 `conn.exec(command, {pty})`,本地侧用
|
|
673
|
+
`sh -c 'export TERM=…; exec <同一个 shell> -c <command>'`——外层 `exec` 让进程就是命令本身
|
|
674
|
+
(退出码与信号忠实),**内层 `-c` 才保证整段命令被当 shell 代码执行**:`cd x && cmd`、
|
|
675
|
+
`a; b`、`for …; do …; done` 都完整跑(D78:旧写法把命令直接缀在 `exec` 后面,只跑第一条,
|
|
676
|
+
内建命令还会直接结束外层 shell)。
|
|
579
677
|
- **命令标签会自动重开**:宿主重启 / 断线重连后 sid 已失效,客户端对
|
|
580
678
|
`spawnSpec.command` 的标签按原规格重新执行命令(普通非持久标签维持「点击重试」
|
|
581
679
|
的旧行为)。页面刷新后同样按原命令恢复。
|
|
582
680
|
- 命令来自**宿主侧插件**(不是远程用户输入),信任级与插件本身相同;tty 只校验
|
|
583
681
|
形状:非空、单行、长度 ≤2000(换行会破坏本地 `-c` 包装层)。
|
|
682
|
+
- **「单行 ≤2000」是这条通道的约束,不是 `tty_open` 的**(2026-09-27 复核指出):
|
|
683
|
+
`spawn` / `ssh` 帧的 `command` 要拼进 `-c` 包装层,所以由 `sanitizeCommand` 拒绝
|
|
684
|
+
换行与 NUL;而 agent 的 `tty_open.command` 走另一条路——命令**整段**用
|
|
685
|
+
`shSingleQuote` 包住交给内层 `-c`(见上一条),**可以含换行**(多行脚本实测可跑),
|
|
686
|
+
只受工具参数的字符串约束。两条通道的入参规则不同,别把这里当 `tty_open` 的限制。
|
|
584
687
|
- 服务名 `ttyTerminal` 同样未声明在 `Context` 类型面上,按可选依赖注入;tty 未安装
|
|
585
688
|
或版本 < 0.14.0 时不会触发(dsh-docker 会退化为「复制命令」)。
|
|
586
689
|
|
|
@@ -706,7 +809,7 @@ ctx.inject(['ttyPanel'], (c) => {
|
|
|
706
809
|
| C→S | `{t:'attach', sid}` | 重连孤儿会话(保活窗口内):`ready(reattached:true)` 后紧跟一帧 `data` 回放输出缓冲 |
|
|
707
810
|
| C→S | `{t:'statsOn' \| 'statsOff', sid}` | 订阅/退订该会话的服务器状态条(0.17.0):按标签可见性驱动,宿主只在有订阅时采集(懒启动 + 退订即停表并关远端 channel) |
|
|
708
811
|
| S→C | `{t:'ready', sid, pid, kind, target?, persist?, reattached?}` | 会话就绪;`kind:'local'` 带 pid,`kind:'ssh'` 时 pid=null、target=user@host[:port];attach 复用此帧并带 `reattached:true`;`persist:true` 表示 tmux 持久会话(0.10.0) |
|
|
709
|
-
| S→C | `{t:'data', sid, d}` | 终端输出(utf8 文本,StringDecoder 兜跨帧多字节序列);**12ms 窗口/64KB 阈值合并成帧**(0.4.1),exit/kill
|
|
812
|
+
| S→C | `{t:'data', sid, d}` | 终端输出(utf8 文本,StringDecoder 兜跨帧多字节序列);**12ms 窗口/64KB 阈值合并成帧**(0.4.1),exit/kill 前强制冲刷保证帧序——**退出前最后一批一定发出**(D76:这一批曾被终局路径自己的 `closed` 守卫整批吞掉,于「打印完就退出」的进程上表现为面板永远少最后几行) |
|
|
710
813
|
| S→C | `{t:'stats', sid, stats}` | 资源指标帧(0.17.0):`{cpuPct, cores, memUsed, memTotal, memPct, diskUsed, diskTotal, diskPct, uptimeSec, tcpConns, rxRate, txRate, tempC?}`;缺失字段即省略(best-effort,前端显示「无」),字节类为 bytes、速率为 B/s |
|
|
711
814
|
| S→C | `{t:'exit', sid, code, signal}` | PTY 退出事实(恰好一次;attach 换连接后仍随当前连接送达) |
|
|
712
815
|
| S→C | `{t:'error', sid?, m}` | 错误 |
|
|
@@ -866,7 +969,11 @@ node scripts/preview.mjs --theme=light # 浅色主题
|
|
|
866
969
|
│ /api/dsh-tty/credential-refs(凭据存储里已知的引用名 —— 只要名字,见「凭据存储」)、
|
|
867
970
|
│ /api/dsh-tty/env-vars(env 插件托管变量名)、/api/dsh-tty/known-hosts
|
|
868
971
|
│ (TOFU 指纹预填充,src/known-hosts.ts 解析含 hashed 条目)、
|
|
869
|
-
│ /api/dsh-tty/shells(Shell 路径候选)——均 loopback
|
|
972
|
+
│ /api/dsh-tty/shells(Shell 路径候选)——均 loopback 围栏(加固档:127/8 全段 +
|
|
973
|
+
│ 别名主机名的 DNS 确认 + 来源检查在 DNS 之前,实现只有一份在 @hyzyn/dsh-kit);
|
|
974
|
+
│ **改状态的端点另需同源证明**(POST /probe、sftp 的 mkdir/rename/remove/upload、
|
|
975
|
+
│ local-fs 的 mkdir/rename/remove/transfer);只读端点(含 sftp /list 与 /download)
|
|
976
|
+
│ 不要求证明,裸 curl 也能读。POST /config 刻意豁免——它是插件被禁用后唯一的恢复入口
|
|
870
977
|
├─ SFTP(src/sftp.ts,0.7.0):懒连接池(空闲 120s 回收、断开按需重连、
|
|
871
978
|
│ TOFU 共用)→ POST /api/dsh-tty/sftp/list|mkdir|rename|remove|download|
|
|
872
979
|
│ upload(spec 走体/头,凭证不进 URL;上传下载流式 pipe)+
|