msdev 0.14.0__tar.gz → 0.15.0__tar.gz

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.
Files changed (114) hide show
  1. {msdev-0.14.0 → msdev-0.15.0}/PKG-INFO +36 -47
  2. {msdev-0.14.0 → msdev-0.15.0}/README.md +35 -46
  3. {msdev-0.14.0 → msdev-0.15.0}/pyproject.toml +1 -1
  4. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/__init__.py +1 -1
  5. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/cli.py +5 -8
  6. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/compat.py +129 -19
  7. msdev-0.15.0/src/msdev/core/daemon_version.py +26 -0
  8. msdev-0.15.0/src/msdev/core/direct_exec.py +178 -0
  9. msdev-0.15.0/src/msdev/core/env_exec.py +173 -0
  10. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/node.py +16 -47
  11. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/transport.py +364 -521
  12. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdevd-linux-amd64 +0 -0
  13. msdev-0.15.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +1 -0
  14. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdevd-linux-arm64 +0 -0
  15. msdev-0.15.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +1 -0
  16. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/skills/msdev-cli/SKILL.md +17 -15
  17. {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/PKG-INFO +36 -47
  18. {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/SOURCES.txt +5 -0
  19. {msdev-0.14.0 → msdev-0.15.0}/tests/test_cli.py +2 -2
  20. msdev-0.15.0/tests/test_compat.py +111 -0
  21. {msdev-0.14.0 → msdev-0.15.0}/tests/test_config_bundle.py +3 -3
  22. msdev-0.15.0/tests/test_connect_env_scenario.py +75 -0
  23. {msdev-0.14.0 → msdev-0.15.0}/tests/test_daemon.py +23 -45
  24. {msdev-0.14.0 → msdev-0.15.0}/tests/test_daemon_deploy.py +2 -0
  25. msdev-0.15.0/tests/test_env_exec_scenario.py +55 -0
  26. {msdev-0.14.0 → msdev-0.15.0}/tests/test_exec_command_scenario.py +3 -55
  27. msdev-0.15.0/tests/test_exec_default_timeout_scenario.py +75 -0
  28. {msdev-0.14.0 → msdev-0.15.0}/tests/test_go_msdevd_scenario.py +1 -1
  29. msdev-0.15.0/tests/test_node_bootstrap_version_scenario.py +78 -0
  30. {msdev-0.14.0 → msdev-0.15.0}/tests/test_ssh_auth.py +6 -13
  31. {msdev-0.14.0 → msdev-0.15.0}/tests/test_target_exec_services.py +34 -18
  32. {msdev-0.14.0 → msdev-0.15.0}/tests/test_transport.py +23 -0
  33. msdev-0.15.0/tests/test_windows_git_bash_ssh_scenario.py +45 -0
  34. msdev-0.14.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +0 -1
  35. msdev-0.14.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +0 -1
  36. msdev-0.14.0/tests/test_compat.py +0 -64
  37. msdev-0.14.0/tests/test_connect_env_scenario.py +0 -146
  38. msdev-0.14.0/tests/test_exec_default_timeout_scenario.py +0 -128
  39. msdev-0.14.0/tests/test_node_bootstrap_version_scenario.py +0 -173
  40. {msdev-0.14.0 → msdev-0.15.0}/LICENSE +0 -0
  41. {msdev-0.14.0 → msdev-0.15.0}/setup.cfg +0 -0
  42. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/__init__.py +0 -0
  43. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/config.py +0 -0
  44. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/config_bundle.py +0 -0
  45. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/container_helper.py +0 -0
  46. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/daemon_deploy.py +0 -0
  47. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/env_transfer.py +0 -0
  48. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/exec_argv.py +0 -0
  49. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/guides.py +0 -0
  50. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/helper_deploy.py +0 -0
  51. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/host_files.py +0 -0
  52. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/host_mutations.py +0 -0
  53. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/init_setup.py +0 -0
  54. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/inventory.py +0 -0
  55. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/limits.py +0 -0
  56. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/local_rpc.py +0 -0
  57. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/multiplex.py +0 -0
  58. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/request_client.py +0 -0
  59. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/resources.py +0 -0
  60. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/__init__.py +0 -0
  61. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/capabilities.py +0 -0
  62. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/context.py +0 -0
  63. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/environment.py +0 -0
  64. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/execution.py +0 -0
  65. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/model.py +0 -0
  66. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/npu.py +0 -0
  67. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/services/results.py +0 -0
  68. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/sftp_transfer.py +0 -0
  69. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/skill_install.py +0 -0
  70. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/ssh_auth.py +0 -0
  71. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/ssh_config.py +0 -0
  72. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/ssh_tunnel.py +0 -0
  73. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/stream_limits.py +0 -0
  74. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/stream_server.py +0 -0
  75. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/stream_transport.py +0 -0
  76. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/core/streamproto.py +0 -0
  77. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/daemon.py +0 -0
  78. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdev-file-helper-linux-amd64 +0 -0
  79. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdev-file-helper-linux-amd64.sha256 +0 -0
  80. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdev-file-helper-linux-arm64 +0 -0
  81. {msdev-0.14.0 → msdev-0.15.0}/src/msdev/helpers/msdev-file-helper-linux-arm64.sha256 +0 -0
  82. {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/dependency_links.txt +0 -0
  83. {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/entry_points.txt +0 -0
  84. {msdev-0.14.0 → msdev-0.15.0}/src/msdev.egg-info/top_level.txt +0 -0
  85. {msdev-0.14.0 → msdev-0.15.0}/tests/test_cli_only_architecture.py +0 -0
  86. {msdev-0.14.0 → msdev-0.15.0}/tests/test_config.py +0 -0
  87. {msdev-0.14.0 → msdev-0.15.0}/tests/test_container_helper.py +0 -0
  88. {msdev-0.14.0 → msdev-0.15.0}/tests/test_daemon_container_stream.py +0 -0
  89. {msdev-0.14.0 → msdev-0.15.0}/tests/test_docker_stream_dispatch.py +0 -0
  90. {msdev-0.14.0 → msdev-0.15.0}/tests/test_env_put_get_scenario.py +0 -0
  91. {msdev-0.14.0 → msdev-0.15.0}/tests/test_env_read_write_scenario.py +0 -0
  92. {msdev-0.14.0 → msdev-0.15.0}/tests/test_env_transfer.py +0 -0
  93. {msdev-0.14.0 → msdev-0.15.0}/tests/test_exec_argv.py +0 -0
  94. {msdev-0.14.0 → msdev-0.15.0}/tests/test_helper_deploy.py +0 -0
  95. {msdev-0.14.0 → msdev-0.15.0}/tests/test_init_skill.py +0 -0
  96. {msdev-0.14.0 → msdev-0.15.0}/tests/test_inventory.py +0 -0
  97. {msdev-0.14.0 → msdev-0.15.0}/tests/test_model_cli.py +0 -0
  98. {msdev-0.14.0 → msdev-0.15.0}/tests/test_model_services.py +0 -0
  99. {msdev-0.14.0 → msdev-0.15.0}/tests/test_msdev_cli_skill.py +0 -0
  100. {msdev-0.14.0 → msdev-0.15.0}/tests/test_multiplex.py +0 -0
  101. {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_connect_provision_scenario.py +0 -0
  102. {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_connect_save_password_scenario.py +0 -0
  103. {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_env_config.py +0 -0
  104. {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_env_services.py +0 -0
  105. {msdev-0.14.0 → msdev-0.15.0}/tests/test_node_sync_ssh_scenario.py +0 -0
  106. {msdev-0.14.0 → msdev-0.15.0}/tests/test_request_client.py +0 -0
  107. {msdev-0.14.0 → msdev-0.15.0}/tests/test_sftp_transfer_scenario.py +0 -0
  108. {msdev-0.14.0 → msdev-0.15.0}/tests/test_ssh_config.py +0 -0
  109. {msdev-0.14.0 → msdev-0.15.0}/tests/test_ssh_tunnel.py +0 -0
  110. {msdev-0.14.0 → msdev-0.15.0}/tests/test_stream_server.py +0 -0
  111. {msdev-0.14.0 → msdev-0.15.0}/tests/test_stream_transport.py +0 -0
  112. {msdev-0.14.0 → msdev-0.15.0}/tests/test_streamproto.py +0 -0
  113. {msdev-0.14.0 → msdev-0.15.0}/tests/test_target_guides.py +0 -0
  114. {msdev-0.14.0 → msdev-0.15.0}/tests/test_windows_local_scenario.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: msdev
3
- Version: 0.14.0
3
+ Version: 0.15.0
4
4
  Summary: Portable nodes and execution environments for msModelSlim development
5
5
  Author: msModelSlim Agent Contributors
6
6
  License-Expression: MulanPSL-2.0
@@ -25,12 +25,13 @@ Dynamic: license-file
25
25
 
26
26
  # msdev
27
27
 
28
- `msdev` exposes three capabilities: **`node`** (connect + host agent),
28
+ `msdev` exposes three capabilities: **`node`** (SSH + connection reuse),
29
29
  **`env`** (shared execution context, shell, files), and **`exec`** (commands).
30
30
 
31
- A Node owns SSH reachability and `msdevd` deployment. An Env owns the full
32
- execution context: host or Docker runtime, conda/venv/uv layers, optional
33
- `setup` script (root switch, proxy, etc.), command `wrap`, and static env vars.
31
+ A Node owns SSH reachability and optional ControlMaster multiplexing. An Env
32
+ owns the full execution context: host or Docker runtime, conda/venv/uv layers,
33
+ optional `setup` script (root switch, proxy, etc.), command `wrap`, and static
34
+ env vars. Commands run over direct SSH (no remote `msdevd`).
34
35
 
35
36
  The CLI is intentionally stateless: execution always names the env as the first
36
37
  `exec` argument.
@@ -45,7 +46,7 @@ python3 -m pip install -e .
45
46
  msdev --help
46
47
  ```
47
48
 
48
- The package installs `msdev` and `msdevd`.
49
+ The package installs the `msdev` CLI.
49
50
 
50
51
  Install the agent skill for Cursor and Claude Code, and sync SSH config:
51
52
 
@@ -63,43 +64,39 @@ overwrite; use `-f` to force.
63
64
 
64
65
  ## Windows client
65
66
 
66
- The typical setup is a **Windows laptop talking to Linux Nodes** via OpenSSH
67
- (`C:\Windows\System32\OpenSSH\ssh.exe`). Native Windows OpenSSH does not
68
- implement ControlMaster multiplexing, so msdev:
69
-
70
- - omits `ControlMaster`/`ControlPath` on Windows (override with
71
- `MSDEV_SSH_CONTROL_MASTER=1` only if your SSH client actually supports mux);
72
- - starts a persistent `ssh -L` LocalForward tunnel automatically on the first
73
- RPC (same role as ControlMaster). Explicit `msdev node connect NODE` is
74
- optional; disable persist with `MSDEV_SSH_LOCAL_FORWARD_PERSIST=0`;
75
- - reuses that tunnel for captured RPC and default streaming `msdev exec`;
76
- `env put`/`get` stay on sftp/ssh streams (file bytes do not go through JSON RPC);
77
- - hides SSH console windows (`CREATE_NO_WINDOW`) and kills process trees with
78
- `taskkill /T` on timeout or cancel;
67
+ The typical setup is a **Windows laptop talking to Linux Nodes**. Native
68
+ `C:\Windows\System32\OpenSSH\ssh.exe` does **not** support ControlMaster.
69
+ msdev therefore prefers **Git for Windows** OpenSSH when installed
70
+ (`C:\Program Files\Git\usr\bin\ssh.exe`), which does:
71
+
72
+ - auto-selects Git Bash `ssh`/`sftp`/`ssh-keygen` over PATH / System32 OpenSSH;
73
+ override with `MSDEV_SSH` / `MSDEV_SFTP` / `MSDEV_SSH_KEYGEN`;
74
+ - enables ControlMaster when the resolved client is Git/MSYS OpenSSH (disable
75
+ with `MSDEV_SSH_CONTROL_MASTER=0`, force with `=1`);
76
+ - falls back to System32 OpenSSH without mux if Git is not installed;
77
+ - hides SSH console windows (`CREATE_NO_WINDOW`) and kills local process trees
78
+ with `taskkill /T` on timeout or cancel;
79
79
  - SSH is non-interactive: `BatchMode` unless a password is supplied, host keys
80
80
  use `accept-new`, and there is no password/passphrase/yes-no prompt. When a
81
81
  password is known, injects OpenSSH `SSH_ASKPASS` so `node connect` (and later
82
82
  ssh/sftp) can authenticate without a TTY; the local public key is then
83
83
  installed on the Node so later logins are passwordless. The password is
84
- never written to the node registry;
85
- - serves a TCP loopback endpoint for local `msdevd` when this Python build has
86
- no `AF_UNIX` sockets.
84
+ never written to the node registry.
87
85
 
88
86
  Install with `python -m pip install -e .`. `install-user.py` writes
89
- `msdev.cmd` / `msdevd.cmd` under `%USERPROFILE%\.local\bin`.
87
+ `msdev.cmd` under `%USERPROFILE%\.local\bin`.
90
88
 
91
89
  Remote paths inside a Linux Env remain POSIX (`/srv/...`). Local `env put`/`get`
92
90
  on Windows accept Windows absolute paths (`C:\...`).
93
91
 
94
92
  Local `exec` on Windows wraps Conda with `conda run` and venvs with
95
93
  `Scripts\activate.bat` (remote Linux Nodes still use POSIX `sh`/`bash` wrappers).
96
- If `ssh` is not on PATH, msdev looks in `%SystemRoot%\System32\OpenSSH\`.
97
94
 
98
95
  ## Nodes and execution environments
99
96
 
100
97
  Use **`node connect`** as the single entry point: it writes `~/.ssh/config`
101
98
  when needed, registers the node, saves passwords, installs the local public
102
- key, and deploys `msdevd`:
99
+ key, and opens an SSH ControlMaster when multiplexing is supported:
103
100
 
104
101
  ```bash
105
102
  msdev node connect dev-122 --hostname 80.48.33.133 --user root --password PASSWORD
@@ -111,25 +108,20 @@ msdev node list
111
108
  ```
112
109
 
113
110
  Connect writes or updates a dedicated `Host <name>` block in OpenSSH config.
114
- The msdev registry stores only the Host alias plus msdevd settings
115
- (`remote_bin`, `auto_bootstrap`). User, port, keys, and proxy remain in SSH
116
- config. `node sync-ssh` registers Host aliases that were added manually.
111
+ The msdev registry stores the Host alias. User, port, keys, and proxy remain in
112
+ SSH config. `node sync-ssh` registers Host aliases that were added manually.
117
113
 
118
- Connect atomically creates a same-named host Env on first use and uploads a
119
- **static Go msdevd** binary matching the node architecture
120
- (`linux-amd64` / `linux-arm64`) when the agent is missing or when the remote
121
- `ping` version does not match the local CLI. Use **`msdev node bootstrap NODE`**
122
- to force a redeploy after upgrading msdev. No Python is required on the node.
114
+ Connect atomically creates a same-named host Env on first use. There is no
115
+ remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
116
+ re-check SSH readiness after connect problems.
123
117
 
124
- Build the static daemons before release or local testing:
118
+ Build optional static helpers before release or local testing:
125
119
 
126
120
  ```bash
127
- bash msdevd/build-static.sh
128
121
  bash file-helper/build-static.sh
129
122
  ```
130
123
 
131
124
  Packaged artifacts live under `src/msdev/helpers/` (gitignored until built).
132
- The Go source is in `msdev/`.
133
125
 
134
126
  Persistent OpenSSH masters are optional. All SSH from msdev is
135
127
  non-interactive: host keys use `accept-new`, and password/passphrase prompts
@@ -259,11 +251,9 @@ CRLF on a Windows console so remote line output does not staircase; piped
259
251
  CLI output keeps LF.
260
252
 
261
253
  `--timeout-seconds` defaults to `-1` (no deadline). Any positive finite duration
262
- is also accepted. A positive timeout kills the complete command
263
- process group and returns exit code 124. Cancelling the local CLI closes the RPC
264
- connection, causing `msdevd` to kill and reap the remote process group. Finite
265
- SSH/Unix RPC deadlines include a short completion grace for output drain and
266
- serialization.
254
+ is also accepted. A positive timeout kills the local SSH/command process tree
255
+ and returns a timed-out result. Cancelling the local CLI closes the SSH
256
+ session (best-effort remote cancel).
267
257
 
268
258
  `--result-json` selects captured, non-streaming execution and prints one
269
259
  structured JSON result containing stdout and stderr.
@@ -273,12 +263,11 @@ structured JSON result containing stdout and stderr.
273
263
  ```text
274
264
  msdev CLI
275
265
  -> typed core service
276
- -> UnixRpcTransport or SshRpcTransport
277
- -> per-user msdevd (env session + job runner + files)
266
+ -> UnixRpcTransport (local subprocess) or SshRpcTransport
267
+ -> SSH ControlMaster (when supported) + direct remote command / SFTP
278
268
  ```
279
269
 
280
- Remote RPC responses and command output are bounded. Node/Env registries use
281
- locked atomic updates.
270
+ Remote command output is bounded. Node/Env registries use locked atomic updates.
282
271
 
283
272
  ## Testing
284
273
 
@@ -287,5 +276,5 @@ python3 -m unittest discover -s tests -v
287
276
  ```
288
277
 
289
278
  Feature work must include at least one actual scenario test on the real
290
- control path (`msdev` CLI → service → transport → `msdevd` → effect). Unit
291
- tests alone are not sufficient.
279
+ control path (`msdev` CLI → service → SSH/SFTP → effect). Unit tests alone
280
+ are not sufficient.
@@ -1,11 +1,12 @@
1
1
  # msdev
2
2
 
3
- `msdev` exposes three capabilities: **`node`** (connect + host agent),
3
+ `msdev` exposes three capabilities: **`node`** (SSH + connection reuse),
4
4
  **`env`** (shared execution context, shell, files), and **`exec`** (commands).
5
5
 
6
- A Node owns SSH reachability and `msdevd` deployment. An Env owns the full
7
- execution context: host or Docker runtime, conda/venv/uv layers, optional
8
- `setup` script (root switch, proxy, etc.), command `wrap`, and static env vars.
6
+ A Node owns SSH reachability and optional ControlMaster multiplexing. An Env
7
+ owns the full execution context: host or Docker runtime, conda/venv/uv layers,
8
+ optional `setup` script (root switch, proxy, etc.), command `wrap`, and static
9
+ env vars. Commands run over direct SSH (no remote `msdevd`).
9
10
 
10
11
  The CLI is intentionally stateless: execution always names the env as the first
11
12
  `exec` argument.
@@ -20,7 +21,7 @@ python3 -m pip install -e .
20
21
  msdev --help
21
22
  ```
22
23
 
23
- The package installs `msdev` and `msdevd`.
24
+ The package installs the `msdev` CLI.
24
25
 
25
26
  Install the agent skill for Cursor and Claude Code, and sync SSH config:
26
27
 
@@ -38,43 +39,39 @@ overwrite; use `-f` to force.
38
39
 
39
40
  ## Windows client
40
41
 
41
- The typical setup is a **Windows laptop talking to Linux Nodes** via OpenSSH
42
- (`C:\Windows\System32\OpenSSH\ssh.exe`). Native Windows OpenSSH does not
43
- implement ControlMaster multiplexing, so msdev:
44
-
45
- - omits `ControlMaster`/`ControlPath` on Windows (override with
46
- `MSDEV_SSH_CONTROL_MASTER=1` only if your SSH client actually supports mux);
47
- - starts a persistent `ssh -L` LocalForward tunnel automatically on the first
48
- RPC (same role as ControlMaster). Explicit `msdev node connect NODE` is
49
- optional; disable persist with `MSDEV_SSH_LOCAL_FORWARD_PERSIST=0`;
50
- - reuses that tunnel for captured RPC and default streaming `msdev exec`;
51
- `env put`/`get` stay on sftp/ssh streams (file bytes do not go through JSON RPC);
52
- - hides SSH console windows (`CREATE_NO_WINDOW`) and kills process trees with
53
- `taskkill /T` on timeout or cancel;
42
+ The typical setup is a **Windows laptop talking to Linux Nodes**. Native
43
+ `C:\Windows\System32\OpenSSH\ssh.exe` does **not** support ControlMaster.
44
+ msdev therefore prefers **Git for Windows** OpenSSH when installed
45
+ (`C:\Program Files\Git\usr\bin\ssh.exe`), which does:
46
+
47
+ - auto-selects Git Bash `ssh`/`sftp`/`ssh-keygen` over PATH / System32 OpenSSH;
48
+ override with `MSDEV_SSH` / `MSDEV_SFTP` / `MSDEV_SSH_KEYGEN`;
49
+ - enables ControlMaster when the resolved client is Git/MSYS OpenSSH (disable
50
+ with `MSDEV_SSH_CONTROL_MASTER=0`, force with `=1`);
51
+ - falls back to System32 OpenSSH without mux if Git is not installed;
52
+ - hides SSH console windows (`CREATE_NO_WINDOW`) and kills local process trees
53
+ with `taskkill /T` on timeout or cancel;
54
54
  - SSH is non-interactive: `BatchMode` unless a password is supplied, host keys
55
55
  use `accept-new`, and there is no password/passphrase/yes-no prompt. When a
56
56
  password is known, injects OpenSSH `SSH_ASKPASS` so `node connect` (and later
57
57
  ssh/sftp) can authenticate without a TTY; the local public key is then
58
58
  installed on the Node so later logins are passwordless. The password is
59
- never written to the node registry;
60
- - serves a TCP loopback endpoint for local `msdevd` when this Python build has
61
- no `AF_UNIX` sockets.
59
+ never written to the node registry.
62
60
 
63
61
  Install with `python -m pip install -e .`. `install-user.py` writes
64
- `msdev.cmd` / `msdevd.cmd` under `%USERPROFILE%\.local\bin`.
62
+ `msdev.cmd` under `%USERPROFILE%\.local\bin`.
65
63
 
66
64
  Remote paths inside a Linux Env remain POSIX (`/srv/...`). Local `env put`/`get`
67
65
  on Windows accept Windows absolute paths (`C:\...`).
68
66
 
69
67
  Local `exec` on Windows wraps Conda with `conda run` and venvs with
70
68
  `Scripts\activate.bat` (remote Linux Nodes still use POSIX `sh`/`bash` wrappers).
71
- If `ssh` is not on PATH, msdev looks in `%SystemRoot%\System32\OpenSSH\`.
72
69
 
73
70
  ## Nodes and execution environments
74
71
 
75
72
  Use **`node connect`** as the single entry point: it writes `~/.ssh/config`
76
73
  when needed, registers the node, saves passwords, installs the local public
77
- key, and deploys `msdevd`:
74
+ key, and opens an SSH ControlMaster when multiplexing is supported:
78
75
 
79
76
  ```bash
80
77
  msdev node connect dev-122 --hostname 80.48.33.133 --user root --password PASSWORD
@@ -86,25 +83,20 @@ msdev node list
86
83
  ```
87
84
 
88
85
  Connect writes or updates a dedicated `Host <name>` block in OpenSSH config.
89
- The msdev registry stores only the Host alias plus msdevd settings
90
- (`remote_bin`, `auto_bootstrap`). User, port, keys, and proxy remain in SSH
91
- config. `node sync-ssh` registers Host aliases that were added manually.
86
+ The msdev registry stores the Host alias. User, port, keys, and proxy remain in
87
+ SSH config. `node sync-ssh` registers Host aliases that were added manually.
92
88
 
93
- Connect atomically creates a same-named host Env on first use and uploads a
94
- **static Go msdevd** binary matching the node architecture
95
- (`linux-amd64` / `linux-arm64`) when the agent is missing or when the remote
96
- `ping` version does not match the local CLI. Use **`msdev node bootstrap NODE`**
97
- to force a redeploy after upgrading msdev. No Python is required on the node.
89
+ Connect atomically creates a same-named host Env on first use. There is no
90
+ remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
91
+ re-check SSH readiness after connect problems.
98
92
 
99
- Build the static daemons before release or local testing:
93
+ Build optional static helpers before release or local testing:
100
94
 
101
95
  ```bash
102
- bash msdevd/build-static.sh
103
96
  bash file-helper/build-static.sh
104
97
  ```
105
98
 
106
99
  Packaged artifacts live under `src/msdev/helpers/` (gitignored until built).
107
- The Go source is in `msdev/`.
108
100
 
109
101
  Persistent OpenSSH masters are optional. All SSH from msdev is
110
102
  non-interactive: host keys use `accept-new`, and password/passphrase prompts
@@ -234,11 +226,9 @@ CRLF on a Windows console so remote line output does not staircase; piped
234
226
  CLI output keeps LF.
235
227
 
236
228
  `--timeout-seconds` defaults to `-1` (no deadline). Any positive finite duration
237
- is also accepted. A positive timeout kills the complete command
238
- process group and returns exit code 124. Cancelling the local CLI closes the RPC
239
- connection, causing `msdevd` to kill and reap the remote process group. Finite
240
- SSH/Unix RPC deadlines include a short completion grace for output drain and
241
- serialization.
229
+ is also accepted. A positive timeout kills the local SSH/command process tree
230
+ and returns a timed-out result. Cancelling the local CLI closes the SSH
231
+ session (best-effort remote cancel).
242
232
 
243
233
  `--result-json` selects captured, non-streaming execution and prints one
244
234
  structured JSON result containing stdout and stderr.
@@ -248,12 +238,11 @@ structured JSON result containing stdout and stderr.
248
238
  ```text
249
239
  msdev CLI
250
240
  -> typed core service
251
- -> UnixRpcTransport or SshRpcTransport
252
- -> per-user msdevd (env session + job runner + files)
241
+ -> UnixRpcTransport (local subprocess) or SshRpcTransport
242
+ -> SSH ControlMaster (when supported) + direct remote command / SFTP
253
243
  ```
254
244
 
255
- Remote RPC responses and command output are bounded. Node/Env registries use
256
- locked atomic updates.
245
+ Remote command output is bounded. Node/Env registries use locked atomic updates.
257
246
 
258
247
  ## Testing
259
248
 
@@ -262,5 +251,5 @@ python3 -m unittest discover -s tests -v
262
251
  ```
263
252
 
264
253
  Feature work must include at least one actual scenario test on the real
265
- control path (`msdev` CLI → service → transport → `msdevd` → effect). Unit
266
- tests alone are not sufficient.
254
+ control path (`msdev` CLI → service → SSH/SFTP → effect). Unit tests alone
255
+ are not sufficient.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "msdev"
7
- version = "0.14.0"
7
+ version = "0.15.0"
8
8
  description = "Portable nodes and execution environments for msModelSlim development"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """Portable node and execution-environment tooling for msModelSlim."""
2
2
 
3
- __version__ = "0.14.0"
3
+ __version__ = "0.15.0"
@@ -619,23 +619,20 @@ def build_parser() -> argparse.ArgumentParser:
619
619
  node_status.add_argument("name")
620
620
  node_bootstrap = node_sub.add_parser(
621
621
  "bootstrap",
622
- help=(
623
- "force-redeploy msdevd on the node "
624
- "(use after local upgrade when remote version mismatches)"
625
- ),
622
+ help="ensure SSH reachability / ControlMaster for the node",
626
623
  )
627
624
  node_bootstrap.add_argument("name")
628
625
  node_bootstrap.add_argument(
629
626
  "--package",
630
627
  type=Path,
631
628
  default=None,
632
- help="optional source checkout for source-layout bootstrap",
629
+ help="ignored (kept for compatibility; remote msdevd is no longer deployed)",
633
630
  )
634
631
  node_connect = node_sub.add_parser(
635
632
  "connect",
636
633
  help=(
637
- "register a node if needed, write ~/.ssh/config, connect, "
638
- "and deploy/start msdevd when needed"
634
+ "register a node if needed, write ~/.ssh/config, "
635
+ "and open an SSH ControlMaster when supported"
639
636
  ),
640
637
  )
641
638
  node_connect.add_argument(
@@ -671,7 +668,7 @@ def build_parser() -> argparse.ArgumentParser:
671
668
  node_connect.add_argument(
672
669
  "--no-auto-bootstrap",
673
670
  action="store_true",
674
- help="do not install/start msdevd automatically on first use",
671
+ help="ignored (kept for compatibility; no remote agent deploy)",
675
672
  )
676
673
  node_connect.add_argument(
677
674
  "--password",
@@ -1,4 +1,4 @@
1
- """Platform helpers for POSIX and native Windows OpenSSH clients."""
1
+ """Platform helpers for POSIX and Windows OpenSSH clients."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -9,6 +9,7 @@ import stat
9
9
  import subprocess
10
10
  import sys
11
11
  import threading
12
+ from functools import lru_cache
12
13
  from pathlib import Path
13
14
  from typing import Any
14
15
 
@@ -25,46 +26,155 @@ def unix_sockets_supported() -> bool:
25
26
  return True
26
27
 
27
28
 
28
- def _windows_openssh(name: str) -> str | None:
29
- root = Path(os.environ.get("SystemRoot") or os.environ.get("WINDIR") or r"C:\Windows")
29
+ def _windows_native_openssh(name: str) -> str | None:
30
+ root = Path(
31
+ os.environ.get("SystemRoot") or os.environ.get("WINDIR") or r"C:\Windows"
32
+ )
30
33
  candidate = root / "System32" / "OpenSSH" / name
31
34
  if candidate.is_file():
32
35
  return str(candidate)
33
36
  return None
34
37
 
35
38
 
39
+ def _windows_git_openssh_dirs() -> list[Path]:
40
+ """Candidate Git for Windows ``usr\\bin`` directories (mux-capable OpenSSH)."""
41
+ dirs: list[Path] = []
42
+ for base in (
43
+ os.environ.get("ProgramW6432"),
44
+ os.environ.get("ProgramFiles"),
45
+ os.environ.get("ProgramFiles(x86)"),
46
+ ):
47
+ if base:
48
+ dirs.append(Path(base) / "Git" / "usr" / "bin")
49
+ local = os.environ.get("LOCALAPPDATA")
50
+ if local:
51
+ dirs.append(Path(local) / "Programs" / "Git" / "usr" / "bin")
52
+ # Deduplicate while preserving order.
53
+ seen: set[str] = set()
54
+ unique: list[Path] = []
55
+ for path in dirs:
56
+ key = str(path).lower()
57
+ if key in seen:
58
+ continue
59
+ seen.add(key)
60
+ unique.append(path)
61
+ return unique
62
+
63
+
64
+ def _windows_git_openssh(name: str) -> str | None:
65
+ for directory in _windows_git_openssh_dirs():
66
+ candidate = directory / name
67
+ if candidate.is_file():
68
+ return str(candidate)
69
+ return None
70
+
71
+
72
+ def _normalize_exe_path(path: str) -> str:
73
+ return path.replace("/", "\\").lower()
74
+
75
+
76
+ def _is_windows_native_openssh(path: str) -> bool:
77
+ normalized = _normalize_exe_path(path)
78
+ return "\\windows\\system32\\openssh\\" in normalized
79
+
80
+
81
+ def _is_windows_git_openssh(path: str) -> bool:
82
+ normalized = _normalize_exe_path(path)
83
+ return "\\git\\usr\\bin\\" in normalized or "\\git\\bin\\" in normalized
84
+
85
+
86
+ def _resolve_windows_tool(name: str, *, path_name: str) -> str:
87
+ """Prefer Git Bash OpenSSH over PATH / native Win32-OpenSSH."""
88
+ override_key = {
89
+ "ssh.exe": "MSDEV_SSH",
90
+ "sftp.exe": "MSDEV_SFTP",
91
+ "ssh-keygen.exe": "MSDEV_SSH_KEYGEN",
92
+ }.get(name)
93
+ if override_key:
94
+ override = os.environ.get(override_key, "").strip()
95
+ if override:
96
+ return override
97
+ git = _windows_git_openssh(name)
98
+ if git:
99
+ return git
100
+ which = shutil.which(path_name)
101
+ if which:
102
+ return which
103
+ native = _windows_native_openssh(name)
104
+ if native:
105
+ return native
106
+ return path_name
107
+
108
+
36
109
  def ssh_executable() -> str:
37
- """Resolve ssh without requiring OpenSSH to be on PATH."""
38
- if shutil.which("ssh"):
39
- return "ssh"
40
- fallback = _windows_openssh("ssh.exe") if IS_WINDOWS else None
41
- return fallback or "ssh"
110
+ """Resolve ssh; on Windows prefer Git for Windows (ControlMaster-capable)."""
111
+ if IS_WINDOWS:
112
+ return _resolve_windows_tool("ssh.exe", path_name="ssh")
113
+ which = shutil.which("ssh")
114
+ return which or "ssh"
42
115
 
43
116
 
44
117
  def sftp_executable() -> str:
45
- """Resolve sftp without requiring OpenSSH to be on PATH."""
46
- if shutil.which("sftp"):
47
- return "sftp"
48
- fallback = _windows_openssh("sftp.exe") if IS_WINDOWS else None
49
- return fallback or "sftp"
118
+ """Resolve sftp; keep it paired with the preferred Windows ssh when possible."""
119
+ if IS_WINDOWS:
120
+ return _resolve_windows_tool("sftp.exe", path_name="sftp")
121
+ which = shutil.which("sftp")
122
+ return which or "sftp"
50
123
 
51
124
 
52
125
  def ssh_keygen_executable() -> str:
53
126
  """Resolve ssh-keygen without requiring OpenSSH to be on PATH."""
54
- if shutil.which("ssh-keygen"):
55
- return "ssh-keygen"
56
- fallback = _windows_openssh("ssh-keygen.exe") if IS_WINDOWS else None
57
- return fallback or "ssh-keygen"
127
+ if IS_WINDOWS:
128
+ return _resolve_windows_tool("ssh-keygen.exe", path_name="ssh-keygen")
129
+ which = shutil.which("ssh-keygen")
130
+ return which or "ssh-keygen"
131
+
132
+
133
+ @lru_cache(maxsize=8)
134
+ def _ssh_version_text(ssh: str) -> str:
135
+ try:
136
+ kwargs: dict[str, Any] = {}
137
+ if IS_WINDOWS:
138
+ kwargs["creationflags"] = CREATE_NO_WINDOW
139
+ completed = subprocess.run(
140
+ [ssh, "-V"],
141
+ capture_output=True,
142
+ check=False,
143
+ timeout=5,
144
+ **kwargs,
145
+ )
146
+ except (OSError, subprocess.TimeoutExpired):
147
+ return ""
148
+ # OpenSSH prints version on stderr.
149
+ return f"{completed.stderr or ''}{completed.stdout or ''}"
150
+
151
+
152
+ def ssh_supports_control_master(ssh: str | None = None) -> bool:
153
+ """Return whether the resolved ssh client can use ControlMaster."""
154
+ path = ssh if ssh is not None else ssh_executable()
155
+ if not IS_WINDOWS:
156
+ return True
157
+ if _is_windows_git_openssh(path):
158
+ return True
159
+ if _is_windows_native_openssh(path):
160
+ return False
161
+ version = _ssh_version_text(path).lower()
162
+ if "openssh_for_windows" in version:
163
+ return False
164
+ # Non-native OpenSSH on PATH (Git/MSYS often) supports mux.
165
+ if "openssh_" in version:
166
+ return True
167
+ return False
58
168
 
59
169
 
60
170
  def ssh_control_master_enabled() -> bool:
61
- """OpenSSH ControlMaster is unsupported by native Windows OpenSSH."""
171
+ """Enable ControlMaster on POSIX, or on Windows with Git Bash OpenSSH."""
62
172
  override = os.environ.get("MSDEV_SSH_CONTROL_MASTER", "").strip().lower()
63
173
  if override in {"0", "false", "no", "off"}:
64
174
  return False
65
175
  if override in {"1", "true", "yes", "on"}:
66
176
  return True
67
- return not IS_WINDOWS
177
+ return ssh_supports_control_master()
68
178
 
69
179
 
70
180
  def popen_kwargs(*, hidden: bool = True, new_group: bool = True) -> dict[str, Any]:
@@ -0,0 +1,26 @@
1
+ """Helpers for comparing local CLI and remote msdevd versions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .. import __version__
8
+
9
+
10
+ def remote_daemon_version_matches(
11
+ ping: Any,
12
+ *,
13
+ expected: str | None = None,
14
+ ) -> bool:
15
+ """Return True when ping reports the same msdevd version as the local CLI."""
16
+ target = expected if expected is not None else __version__
17
+ if not isinstance(ping, dict):
18
+ return False
19
+ remote = ping.get("version")
20
+ return isinstance(remote, str) and remote == target
21
+
22
+
23
+ def is_incompatible_daemon_rpc_error(exc: BaseException) -> bool:
24
+ """True when the remote daemon rejected a method the current CLI requires."""
25
+ text = str(exc).lower()
26
+ return "unknown rpc method" in text