dsh-pve-vm 0.1.0 → 0.1.1

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/CHANGELOG.md CHANGED
@@ -3,6 +3,43 @@
3
3
  All notable changes to this plugin are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follow [SemVer](https://semver.org/).
5
5
 
6
+ ## [0.1.1] — 2026-10-13
7
+
8
+ Linux guest support. Everything below was found by running the plugin against a real RedOS 8
9
+ guest with a MATE desktop, not by reading the code.
10
+
11
+ ### Fixed
12
+
13
+ * **Guest OS detection rejected almost every Linux distribution.** The guest agent reports `id`
14
+ from `/etc/os-release`, and only IDs containing `linux` were accepted — RedOS reports `redos`,
15
+ Debian `debian`, Ubuntu `ubuntu`. All of them landed in `other` and every `vm_*` tool answered
16
+ *running a shell command is not supported for guest OS «other»*, so the whole guest-agent half
17
+ was dead on Linux. Windows was unaffected (`mswindows`), which is why it went unnoticed.
18
+ Windows is now matched first, Linux by `id`/`name`/`pretty-name`, and a non-Windows guest that
19
+ answered `get-osinfo` with a non-empty `kernel-release` counts as Linux.
20
+ * **`vm_type_text` typed nothing on a GDM desktop.** The built-in typing script used
21
+ `$home/.Xauthority`, which does not exist when GDM owns the session — RedOS and RHEL keep the
22
+ file at `/run/user/<uid>/gdm/Xauthority`. `xdotool` then talked to the X server with no
23
+ authorization at all, failed with *No protocol specified*, and the text never appeared. The
24
+ script now tries `~/.Xauthority`, then the GDM path, then any `/run/user/<uid>/*/Xauthority`.
25
+ * **`vm_type_text` could not submit text.** `text=$(… | base64 -d)` — command substitution strips
26
+ trailing newlines, so text ending in `"\n"` was typed without pressing Enter and the shell
27
+ prompt never came back.
28
+ * **Console errors hid the actual cause.** When the node's `sshd` does not accept `AcceptEnv`,
29
+ `LC_PVE_TICKET` never arrives and `qm vncproxy` exits with a precise
30
+ *LC_PVE_TICKET not set, VNC proxy without password is forbidden* — but the RFB reader saw EOF
31
+ before ssh had finished exiting, so only *the VNC stream was closed by the node* surfaced. The
32
+ ssh stderr is now reported (after a short grace period), both during the handshake and when a
33
+ live session dies mid-way.
34
+
35
+ ### Known limitations
36
+
37
+ * Proxmox applies a **default 5-second timeout to QMP commands** (`PVE/QMPClient.pm`), and
38
+ `guest-exec` waits for the guest agent to acknowledge. While a previous long command is still
39
+ running inside the guest that acknowledgement arrives too late and the call fails with
40
+ *qmp command 'guest-exec' failed - got timeout*. On an idle guest 10 out of 10 calls complete
41
+ in about a second. `vm_exec` is currently synchronous and does not retry.
42
+
6
43
  ## [0.1.0] — 2026-10-13
7
44
 
8
45
  Initial public release.
package/lib/config.js CHANGED
@@ -12,7 +12,11 @@ import z from '@deepseek-ai/schemastery';
12
12
  /** Text input script inside a Linux guest (used by `vm_type_text`). */
13
13
  export const DEFAULT_GUEST_TYPE_COMMAND = [
14
14
  'set -e',
15
- 'text=$(printf %s "$1" | base64 -d)',
15
+ // `$(...)` strips trailing newlines, and without them the text cannot be
16
+ // "submitted" with Enter: `vm_type_text` with a trailing \"\\n\" silently typed
17
+ // the line and left the prompt waiting.
18
+ 'text=$(printf %s "$1" | base64 -d; printf X)',
19
+ 'text=${text%X}',
16
20
  'uid=""; home=""; disp=""; xauth=""',
17
21
  'for s in $(loginctl list-sessions --no-legend 2>/dev/null | awk \'{print $1}\'); do',
18
22
  ' [ "$(loginctl show-session "$s" -p Active --value 2>/dev/null)" = "yes" ] || continue',
@@ -22,7 +26,15 @@ export const DEFAULT_GUEST_TYPE_COMMAND = [
22
26
  ' [ -n "$disp" ] && break',
23
27
  'done',
24
28
  '[ -n "$disp" ] || disp=":0"',
25
- '[ -n "$home" ] && [ -f "$home/.Xauthority" ] && xauth="$home/.Xauthority"',
29
+ '[ -n "$uid" ] || uid=$(id -u)',
30
+ // XAUTHORITY. GDM (RedOS 8, RHEL) keeps it in /run/user/<uid>/gdm/Xauthority,
31
+ // and ~/.Xauthority may not exist at all — xdotool then talked to X with no
32
+ // authorization whatsoever, got "No protocol specified" and typed nothing
33
+ // (found during acceptance on 2026-10-13, RedOS 8 + MATE + GDM).
34
+ 'if [ -n "$home" ] && [ -f "$home/.Xauthority" ]; then xauth="$home/.Xauthority"',
35
+ 'elif [ -f "/run/user/$uid/gdm/Xauthority" ]; then xauth="/run/user/$uid/gdm/Xauthority"',
36
+ 'else xauth=$(ls -1 /run/user/$uid/*/Xauthority /run/user/$uid/xauth* 2>/dev/null | head -1)',
37
+ 'fi',
26
38
  'run() { if [ -n "$xauth" ]; then XAUTHORITY="$xauth" DISPLAY="$disp" "$@"; else DISPLAY="$disp" "$@"; fi; }',
27
39
  'if command -v xdotool >/dev/null 2>&1; then run xdotool type --delay 12 --clearmodifiers -- "$text"; exit 0; fi',
28
40
  'if command -v wtype >/dev/null 2>&1; then',
package/lib/session.js CHANGED
@@ -252,6 +252,19 @@ export class VmController {
252
252
  } catch (error) {
253
253
  session.alive = false;
254
254
  this.#drop(vmid);
255
+ // RFB sees EOF before ssh has finished exiting, and the most valuable
256
+ // explanation — the node's stderr — is lost. Give ssh a short grace
257
+ // period: without it "the VNC stream was closed by the node" cannot tell
258
+ // an sshd without AcceptEnv (`LC_PVE_TICKET not set, VNC proxy without
259
+ // password is forbidden`) from a powered-off VM or a busy QMP socket.
260
+ // Verified by a negative test on 2026-10-13 (sshd on :2222, no AcceptEnv).
261
+ const detail = await Promise.race([
262
+ handle.done.then(() => stderr.trim()).catch(() => ''),
263
+ new Promise((resolve) => setTimeout(() => resolve(''), 1500)),
264
+ ]);
265
+ if (detail.length > 0 && error?.code === 'EOF') {
266
+ throw new VmError(`failed to open the console of VM ${vmid}: ${detail}`, 'CONSOLE_FAILED');
267
+ }
255
268
  throw error;
256
269
  }
257
270
  this.sessions.set(vmid, session);
@@ -281,6 +294,12 @@ export class VmController {
281
294
  if (error?.code === 'EOF' || error?.code === 'TIMEOUT') {
282
295
  session.alive = false;
283
296
  this.#drop(id);
297
+ // A stream that died mid-way is explained by ssh's stderr: for
298
+ // instance `qm vncproxy` exiting on its own 15-minute idle timeout.
299
+ const detail = session.stderrText?.() ?? '';
300
+ if (detail.length > 0) {
301
+ throw new VmError(`${error.message}; ssh: ${detail}`, error.code);
302
+ }
284
303
  }
285
304
  throw error;
286
305
  }
package/lib/ssh.js CHANGED
@@ -72,13 +72,30 @@ export function guestPingCommand(vmid) {
72
72
 
73
73
  /**
74
74
  * Detect the guest OS from the agent's response.
75
+ *
76
+ * The guest agent reports `id` from `/etc/os-release` (`ID=`), and every distro
77
+ * uses its own name there: `redos`, `debian`, `ubuntu`, `altlinux`, `astra-linux`…
78
+ * So "id == linux" is not a usable Linux test — it filtered out almost every real
79
+ * distribution (found during acceptance on 2026-10-13: RedOS 8 reports `redos`,
80
+ * Debian reports `debian`; both landed in `other` and every `vm_*` tool refused
81
+ * to work). Windows must be checked first: its agent also reports
82
+ * `kernel-release` (the NT build number), so "has kernel-release => Linux" is
83
+ * wrong on its own — the order of the checks matters.
84
+ *
75
85
  * @param {unknown} osinfo the parsed `get-osinfo` JSON.
76
86
  * @returns {'windows'|'linux'|'other'}
77
87
  */
78
88
  export function guestOsFamily(osinfo) {
79
89
  const id = String(osinfo?.id ?? '').toLowerCase();
80
- if (id.includes('windows') || id === 'mswindows') return 'windows';
81
- if (id === 'linux' || id.includes('linux')) return 'linux';
90
+ const name = String(osinfo?.name ?? '').toLowerCase();
91
+ const pretty = String(osinfo?.['pretty-name'] ?? '').toLowerCase();
92
+ if (id === 'mswindows' || id.includes('windows') || name.includes('windows')) return 'windows';
93
+ if (id.includes('linux') || name.includes('linux') || pretty.includes('linux')) return 'linux';
94
+ // Whatever is left is a non-Windows guest that answered get-osinfo. Only
95
+ // Windows and Linux are supported, so it is Linux; `kernel-release` is a
96
+ // sanity check on the reply (an empty agent response never reaches here).
97
+ const kernel = String(osinfo?.['kernel-release'] ?? '');
98
+ if (kernel.length > 0) return 'linux';
82
99
  return 'other';
83
100
  }
84
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pve-vm",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Proxmox VE VM control for DeepSeek Harness: see the screen and click in a VM console over VNC, plus commands, files and typed text through the QEMU guest agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",