msdev 0.15.0__tar.gz → 0.15.1__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 (112) hide show
  1. {msdev-0.15.0 → msdev-0.15.1}/PKG-INFO +13 -6
  2. {msdev-0.15.0 → msdev-0.15.1}/README.md +12 -5
  3. {msdev-0.15.0 → msdev-0.15.1}/pyproject.toml +1 -4
  4. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/__init__.py +1 -1
  5. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/cli.py +54 -29
  6. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/config.py +0 -17
  7. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/guides.py +12 -0
  8. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/inventory.py +1 -1
  9. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/limits.py +19 -1
  10. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/resources.py +50 -6
  11. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/context.py +1 -6
  12. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/environment.py +5 -6
  13. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/node.py +94 -36
  14. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/ssh_config.py +42 -0
  15. msdev-0.15.1/src/msdev/core/transport.py +1021 -0
  16. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/skills/msdev-cli/SKILL.md +20 -5
  17. {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/PKG-INFO +13 -6
  18. {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/SOURCES.txt +2 -36
  19. {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/entry_points.txt +0 -1
  20. {msdev-0.15.0 → msdev-0.15.1}/tests/test_cli.py +49 -16
  21. {msdev-0.15.0 → msdev-0.15.1}/tests/test_cli_only_architecture.py +1 -1
  22. {msdev-0.15.0 → msdev-0.15.1}/tests/test_compat.py +0 -12
  23. {msdev-0.15.0 → msdev-0.15.1}/tests/test_config.py +0 -18
  24. {msdev-0.15.0 → msdev-0.15.1}/tests/test_env_transfer.py +5 -6
  25. msdev-0.15.1/tests/test_live_dev122_dev186_scenario.py +196 -0
  26. msdev-0.15.1/tests/test_local_docker_ssh_smoke_scenario.py +1078 -0
  27. {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_bootstrap_version_scenario.py +2 -3
  28. {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_connect_provision_scenario.py +1 -1
  29. {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_connect_save_password_scenario.py +1 -1
  30. {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_env_config.py +65 -0
  31. {msdev-0.15.0 → msdev-0.15.1}/tests/test_ssh_auth.py +9 -63
  32. {msdev-0.15.0 → msdev-0.15.1}/tests/test_target_exec_services.py +70 -22
  33. msdev-0.15.1/tests/test_transport.py +379 -0
  34. {msdev-0.15.0 → msdev-0.15.1}/tests/test_windows_local_scenario.py +2 -19
  35. msdev-0.15.0/src/msdev/core/container_helper.py +0 -251
  36. msdev-0.15.0/src/msdev/core/daemon_deploy.py +0 -49
  37. msdev-0.15.0/src/msdev/core/daemon_version.py +0 -26
  38. msdev-0.15.0/src/msdev/core/helper_deploy.py +0 -143
  39. msdev-0.15.0/src/msdev/core/host_files.py +0 -92
  40. msdev-0.15.0/src/msdev/core/host_mutations.py +0 -332
  41. msdev-0.15.0/src/msdev/core/local_rpc.py +0 -85
  42. msdev-0.15.0/src/msdev/core/multiplex.py +0 -143
  43. msdev-0.15.0/src/msdev/core/request_client.py +0 -174
  44. msdev-0.15.0/src/msdev/core/ssh_tunnel.py +0 -561
  45. msdev-0.15.0/src/msdev/core/stream_limits.py +0 -17
  46. msdev-0.15.0/src/msdev/core/stream_server.py +0 -606
  47. msdev-0.15.0/src/msdev/core/stream_transport.py +0 -376
  48. msdev-0.15.0/src/msdev/core/streamproto.py +0 -129
  49. msdev-0.15.0/src/msdev/core/transport.py +0 -1498
  50. msdev-0.15.0/src/msdev/daemon.py +0 -1499
  51. msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-amd64 +0 -0
  52. msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-amd64.sha256 +0 -1
  53. msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-arm64 +0 -0
  54. msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-arm64.sha256 +0 -1
  55. msdev-0.15.0/src/msdev/helpers/msdevd-linux-amd64 +0 -0
  56. msdev-0.15.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +0 -1
  57. msdev-0.15.0/src/msdev/helpers/msdevd-linux-arm64 +0 -0
  58. msdev-0.15.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +0 -1
  59. msdev-0.15.0/tests/test_container_helper.py +0 -234
  60. msdev-0.15.0/tests/test_daemon.py +0 -1092
  61. msdev-0.15.0/tests/test_daemon_container_stream.py +0 -84
  62. msdev-0.15.0/tests/test_daemon_deploy.py +0 -150
  63. msdev-0.15.0/tests/test_docker_stream_dispatch.py +0 -237
  64. msdev-0.15.0/tests/test_go_msdevd_scenario.py +0 -209
  65. msdev-0.15.0/tests/test_helper_deploy.py +0 -185
  66. msdev-0.15.0/tests/test_multiplex.py +0 -199
  67. msdev-0.15.0/tests/test_request_client.py +0 -241
  68. msdev-0.15.0/tests/test_ssh_tunnel.py +0 -57
  69. msdev-0.15.0/tests/test_stream_server.py +0 -510
  70. msdev-0.15.0/tests/test_stream_transport.py +0 -314
  71. msdev-0.15.0/tests/test_streamproto.py +0 -65
  72. msdev-0.15.0/tests/test_transport.py +0 -1252
  73. {msdev-0.15.0 → msdev-0.15.1}/LICENSE +0 -0
  74. {msdev-0.15.0 → msdev-0.15.1}/setup.cfg +0 -0
  75. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/__init__.py +0 -0
  76. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/compat.py +0 -0
  77. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/config_bundle.py +0 -0
  78. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/direct_exec.py +0 -0
  79. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/env_exec.py +0 -0
  80. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/env_transfer.py +0 -0
  81. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/exec_argv.py +0 -0
  82. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/init_setup.py +0 -0
  83. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/__init__.py +0 -0
  84. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/capabilities.py +0 -0
  85. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/execution.py +0 -0
  86. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/model.py +0 -0
  87. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/npu.py +0 -0
  88. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/results.py +0 -0
  89. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/sftp_transfer.py +0 -0
  90. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/skill_install.py +0 -0
  91. {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/ssh_auth.py +0 -0
  92. {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/dependency_links.txt +0 -0
  93. {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/top_level.txt +0 -0
  94. {msdev-0.15.0 → msdev-0.15.1}/tests/test_config_bundle.py +0 -0
  95. {msdev-0.15.0 → msdev-0.15.1}/tests/test_connect_env_scenario.py +0 -0
  96. {msdev-0.15.0 → msdev-0.15.1}/tests/test_env_exec_scenario.py +0 -0
  97. {msdev-0.15.0 → msdev-0.15.1}/tests/test_env_put_get_scenario.py +0 -0
  98. {msdev-0.15.0 → msdev-0.15.1}/tests/test_env_read_write_scenario.py +0 -0
  99. {msdev-0.15.0 → msdev-0.15.1}/tests/test_exec_argv.py +0 -0
  100. {msdev-0.15.0 → msdev-0.15.1}/tests/test_exec_command_scenario.py +0 -0
  101. {msdev-0.15.0 → msdev-0.15.1}/tests/test_exec_default_timeout_scenario.py +0 -0
  102. {msdev-0.15.0 → msdev-0.15.1}/tests/test_init_skill.py +0 -0
  103. {msdev-0.15.0 → msdev-0.15.1}/tests/test_inventory.py +0 -0
  104. {msdev-0.15.0 → msdev-0.15.1}/tests/test_model_cli.py +0 -0
  105. {msdev-0.15.0 → msdev-0.15.1}/tests/test_model_services.py +0 -0
  106. {msdev-0.15.0 → msdev-0.15.1}/tests/test_msdev_cli_skill.py +0 -0
  107. {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_env_services.py +0 -0
  108. {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_sync_ssh_scenario.py +0 -0
  109. {msdev-0.15.0 → msdev-0.15.1}/tests/test_sftp_transfer_scenario.py +0 -0
  110. {msdev-0.15.0 → msdev-0.15.1}/tests/test_ssh_config.py +0 -0
  111. {msdev-0.15.0 → msdev-0.15.1}/tests/test_target_guides.py +0 -0
  112. {msdev-0.15.0 → msdev-0.15.1}/tests/test_windows_git_bash_ssh_scenario.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: msdev
3
- Version: 0.15.0
3
+ Version: 0.15.1
4
4
  Summary: Portable nodes and execution environments for msModelSlim development
5
5
  Author: msModelSlim Agent Contributors
6
6
  License-Expression: MulanPSL-2.0
@@ -115,13 +115,20 @@ Connect atomically creates a same-named host Env on first use. There is no
115
115
  remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
116
116
  re-check SSH readiness after connect problems.
117
117
 
118
- Build optional static helpers before release or local testing:
118
+ Inspect connectivity for one node or for every registered node (probes run in
119
+ parallel; partial failures exit non-zero):
119
120
 
120
121
  ```bash
121
- bash file-helper/build-static.sh
122
+ msdev node status
123
+ msdev node status dev-122
122
124
  ```
123
125
 
124
- Packaged artifacts live under `src/msdev/helpers/` (gitignored until built).
126
+ Rename a node together with its same-named host Env, guide files, and SSH
127
+ `Host` alias:
128
+
129
+ ```bash
130
+ msdev node rename dev-122 lab-122
131
+ ```
125
132
 
126
133
  Persistent OpenSSH masters are optional. All SSH from msdev is
127
134
  non-interactive: host keys use `accept-new`, and password/passphrase prompts
@@ -171,11 +178,11 @@ msdev env add dev-122-root \
171
178
  msdev env open dev-122-root
172
179
  ```
173
180
 
174
- Open a human-operated interactive shell in an Env:
181
+ Open an interactive shell in an env. Docker envs enter the container:
175
182
 
176
183
  ```bash
177
184
  msdev env shell dev-122
178
- msdev env shell dev-122 --cwd /srv/project
185
+ msdev env shell dev-122-docker --cwd /srv/project
179
186
  ```
180
187
 
181
188
  Agent automation should use `msdev exec`, not `env shell`.
@@ -90,13 +90,20 @@ Connect atomically creates a same-named host Env on first use. There is no
90
90
  remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
91
91
  re-check SSH readiness after connect problems.
92
92
 
93
- Build optional static helpers before release or local testing:
93
+ Inspect connectivity for one node or for every registered node (probes run in
94
+ parallel; partial failures exit non-zero):
94
95
 
95
96
  ```bash
96
- bash file-helper/build-static.sh
97
+ msdev node status
98
+ msdev node status dev-122
97
99
  ```
98
100
 
99
- Packaged artifacts live under `src/msdev/helpers/` (gitignored until built).
101
+ Rename a node together with its same-named host Env, guide files, and SSH
102
+ `Host` alias:
103
+
104
+ ```bash
105
+ msdev node rename dev-122 lab-122
106
+ ```
100
107
 
101
108
  Persistent OpenSSH masters are optional. All SSH from msdev is
102
109
  non-interactive: host keys use `accept-new`, and password/passphrase prompts
@@ -146,11 +153,11 @@ msdev env add dev-122-root \
146
153
  msdev env open dev-122-root
147
154
  ```
148
155
 
149
- Open a human-operated interactive shell in an Env:
156
+ Open an interactive shell in an env. Docker envs enter the container:
150
157
 
151
158
  ```bash
152
159
  msdev env shell dev-122
153
- msdev env shell dev-122 --cwd /srv/project
160
+ msdev env shell dev-122-docker --cwd /srv/project
154
161
  ```
155
162
 
156
163
  Agent automation should use `msdev exec`, not `env shell`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "msdev"
7
- version = "0.15.0"
7
+ version = "0.15.1"
8
8
  description = "Portable nodes and execution environments for msModelSlim development"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -35,7 +35,6 @@ Repository = "https://gitcode.com/rookie_hongchuan/msdev"
35
35
 
36
36
  [project.scripts]
37
37
  msdev = "msdev.cli:main"
38
- msdevd = "msdev.daemon:main"
39
38
 
40
39
  [tool.setuptools]
41
40
  package-dir = { "" = "src" }
@@ -45,8 +44,6 @@ where = ["src"]
45
44
 
46
45
  [tool.setuptools.package-data]
47
46
  msdev = [
48
- "helpers/msdev-file-helper-*",
49
- "helpers/msdevd-linux-*",
50
47
  "skills/msdev-cli/SKILL.md",
51
48
  ]
52
49
 
@@ -1,3 +1,3 @@
1
1
  """Portable node and execution-environment tooling for msModelSlim."""
2
2
 
3
- __version__ = "0.15.0"
3
+ __version__ = "0.15.1"
@@ -154,8 +154,6 @@ def _provision_request_for_connect(
154
154
  if ssh_host_defined(args.name):
155
155
  return NodeAddRequest(
156
156
  name=args.name,
157
- remote_bin=args.remote_bin,
158
- auto_bootstrap=not args.no_auto_bootstrap,
159
157
  force=args.force,
160
158
  )
161
159
  raise ValueError(
@@ -169,8 +167,6 @@ def _provision_request_for_connect(
169
167
  port=args.port,
170
168
  identity_file=args.identity_file,
171
169
  ssh_host=args.ssh_host,
172
- remote_bin=args.remote_bin,
173
- auto_bootstrap=not args.no_auto_bootstrap,
174
170
  force=bool(
175
171
  args.force or has_params
176
172
  ),
@@ -204,9 +200,7 @@ def command_node(
204
200
  print("local\tregistered\t-")
205
201
  for node in service.list().nodes:
206
202
  print(
207
- f"{node.name}\tregistered\tssh={node.ssh_host}\t"
208
- f"auto-bootstrap={'on' if node.auto_bootstrap else 'off'}\t"
209
- f"bin={node.remote_bin}"
203
+ f"{node.name}\tregistered\tssh={node.ssh_host}"
210
204
  )
211
205
  if args.ssh:
212
206
  for host in service.list_ssh_hosts():
@@ -236,12 +230,39 @@ def command_node(
236
230
  if removed:
237
231
  print("removed environments: " + ", ".join(removed))
238
232
  return 0
239
- if action == "status":
240
- print(f"node: {args.name}")
241
- _print_json(service.status(args.name))
233
+ if action == "rename":
234
+ node = service.rename(args.name, args.new_name)
235
+ print(f"renamed node {args.name} -> {node.name} (ssh={node.ssh_host})")
242
236
  return 0
237
+ if action == "status":
238
+ if args.name:
239
+ print(f"node: {args.name}")
240
+ _print_json(service.status(args.name))
241
+ return 0
242
+ result = service.status_all()
243
+ for item in result.items:
244
+ if item.error is not None:
245
+ print(
246
+ f"{item.node} ({item.ssh_host}): unreachable: {item.error}",
247
+ file=sys.stderr,
248
+ )
249
+ else:
250
+ info = item.info if isinstance(item.info, dict) else {}
251
+ parts: list[str] = []
252
+ hostname = str(info.get("hostname") or "")
253
+ ip = str(info.get("ip") or "")
254
+ target = str(info.get("target") or "")
255
+ if hostname:
256
+ parts.append(f"hostname={hostname}")
257
+ if ip:
258
+ parts.append(f"ip={ip}")
259
+ if target and target not in {hostname, ip, item.ssh_host}:
260
+ parts.append(f"target={target}")
261
+ detail = (" " + " ".join(parts)) if parts else ""
262
+ print(f"{item.node} ({item.ssh_host}): reachable{detail}")
263
+ return result.exit_code
243
264
  if action == "bootstrap":
244
- result = service.bootstrap(args.name, package_path=args.package)
265
+ result = service.bootstrap(args.name)
245
266
  _print_json(result)
246
267
  return 0
247
268
  if action in {"connect", "disconnect"}:
@@ -281,8 +302,8 @@ def command_node(
281
302
  key = getattr(item, "key", None)
282
303
  if key:
283
304
  suffix = f"(persist={item.persist}, key={key})"
284
- if item.agent_ready:
285
- suffix = suffix[:-1] + ", agent=ready)"
305
+ if item.ssh_ready:
306
+ suffix = suffix[:-1] + ", ssh=ready)"
286
307
  print(
287
308
  f"{item.node}: {item.state} to {item.ssh_host} "
288
309
  f"{suffix}"
@@ -615,19 +636,26 @@ def build_parser() -> argparse.ArgumentParser:
615
636
  action="store_true",
616
637
  help="also remove every environment on this node",
617
638
  )
618
- node_status = node_sub.add_parser("status")
619
- node_status.add_argument("name")
639
+ node_rename = node_sub.add_parser(
640
+ "rename",
641
+ help="rename a node, its host environment, and a matching SSH Host alias",
642
+ )
643
+ node_rename.add_argument("name")
644
+ node_rename.add_argument("new_name")
645
+ node_status = node_sub.add_parser(
646
+ "status",
647
+ help="check node connectivity (every registered SSH node by default)",
648
+ )
649
+ node_status.add_argument(
650
+ "name",
651
+ nargs="?",
652
+ help="check one node; omit to check every registered SSH node",
653
+ )
620
654
  node_bootstrap = node_sub.add_parser(
621
655
  "bootstrap",
622
656
  help="ensure SSH reachability / ControlMaster for the node",
623
657
  )
624
658
  node_bootstrap.add_argument("name")
625
- node_bootstrap.add_argument(
626
- "--package",
627
- type=Path,
628
- default=None,
629
- help="ignored (kept for compatibility; remote msdevd is no longer deployed)",
630
- )
631
659
  node_connect = node_sub.add_parser(
632
660
  "connect",
633
661
  help=(
@@ -659,17 +687,11 @@ def build_parser() -> argparse.ArgumentParser:
659
687
  "--ssh-host",
660
688
  help="legacy shorthand for user@hostname when writing ~/.ssh/config",
661
689
  )
662
- node_connect.add_argument("--remote-bin", default="~/.local/bin/msdevd")
663
690
  node_connect.add_argument(
664
691
  "--force",
665
692
  action="store_true",
666
693
  help="replace an existing node and its same-named host environment",
667
694
  )
668
- node_connect.add_argument(
669
- "--no-auto-bootstrap",
670
- action="store_true",
671
- help="ignored (kept for compatibility; no remote agent deploy)",
672
- )
673
695
  node_connect.add_argument(
674
696
  "--password",
675
697
  default=None,
@@ -749,7 +771,7 @@ def build_parser() -> argparse.ArgumentParser:
749
771
  environment_inspect.add_argument("name")
750
772
  environment_open = environment_sub.add_parser(
751
773
  "open",
752
- help="establish the shared environment session (runs setup once)",
774
+ help="check that the environment is ready (setup runs on each exec)",
753
775
  )
754
776
  environment_open.add_argument("name")
755
777
  environment_guide = environment_sub.add_parser("guide")
@@ -757,7 +779,10 @@ def build_parser() -> argparse.ArgumentParser:
757
779
  environment_guide_action = environment_guide.add_mutually_exclusive_group()
758
780
  environment_guide_action.add_argument("--write", metavar="FILE")
759
781
  environment_guide_action.add_argument("--delete", action="store_true")
760
- environment_shell = environment_sub.add_parser("shell")
782
+ environment_shell = environment_sub.add_parser(
783
+ "shell",
784
+ help="open an interactive shell in the environment (including Docker)",
785
+ )
761
786
  environment_shell.add_argument("name")
762
787
  environment_shell.add_argument("--cwd")
763
788
  environment_put = environment_sub.add_parser(
@@ -354,20 +354,3 @@ def config_home() -> Path:
354
354
  return Path(override).expanduser()
355
355
  base = os.environ.get("XDG_CONFIG_HOME")
356
356
  return Path(base).expanduser() / "msdev" if base else Path.home() / ".config" / "msdev"
357
-
358
-
359
- def data_home() -> Path:
360
- override = os.environ.get("MSDEV_DATA_HOME")
361
- if override:
362
- return Path(override).expanduser()
363
- base = os.environ.get("XDG_DATA_HOME")
364
- return Path(base).expanduser() / "msdevd" if base else Path.home() / ".local" / "share" / "msdevd"
365
-
366
-
367
- def default_socket_path() -> Path:
368
- override = os.environ.get("MSDEVD_SOCKET")
369
- if override:
370
- return Path(override).expanduser()
371
- # SSH sessions frequently lack XDG_RUNTIME_DIR while systemd user services
372
- # set it to /run/user/<uid>. A stable per-user path keeps both sides aligned.
373
- return data_home() / "msdevd.sock"
@@ -89,6 +89,18 @@ class ResourceGuideStore:
89
89
  except FileNotFoundError:
90
90
  return False
91
91
 
92
+ def rename(self, old: str, new: str) -> bool:
93
+ """Move a guide file. Return whether one existed."""
94
+ source = self.path(old)
95
+ if not source.exists():
96
+ return False
97
+ destination = self.path(new)
98
+ if destination.exists():
99
+ raise ValueError(f"guide already exists for {new!r}")
100
+ destination.parent.mkdir(parents=True, exist_ok=True)
101
+ os.replace(source, destination)
102
+ return True
103
+
92
104
 
93
105
  class NodeGuideStore(ResourceGuideStore):
94
106
  def __init__(self, root: Path | None = None):
@@ -44,7 +44,7 @@ def _utc_now() -> str:
44
44
 
45
45
 
46
46
  class Inventory:
47
- """SQLite inventory owned by one user-level daemon."""
47
+ """SQLite inventory for model artifacts and replicas."""
48
48
 
49
49
  def __init__(self, data_dir: Path):
50
50
  self.data_dir = data_dir.expanduser()
@@ -10,9 +10,15 @@ MAX_RPC_RESPONSE_BYTES = 32 * 1024 * 1024
10
10
  MAX_RPC_ERROR_BYTES = 1 * 1024 * 1024
11
11
 
12
12
  DEFAULT_EXEC_TIMEOUT_SECONDS = -1.0
13
- EXEC_RPC_COMPLETION_GRACE_SECONDS = 10.0
14
13
  RPC_PROCESS_CLEANUP_GRACE_SECONDS = 2.0
15
14
 
15
+ # Infrastructure budget for ControlMaster probe / discard / (re)connect.
16
+ # Separate from exec command deadline (DEFAULT_EXEC_TIMEOUT_SECONDS = -1).
17
+ DEFAULT_SSH_SETUP_TIMEOUT_SECONDS = 15.0
18
+ DEFAULT_SSH_MUX_PROBE_TIMEOUT_SECONDS = 5.0
19
+ # Per-node budget for ``msdev node status`` (parallel; no ControlMaster setup).
20
+ DEFAULT_SSH_STATUS_TIMEOUT_SECONDS = 3.0
21
+
16
22
 
17
23
  def validate_exec_timeout_seconds(value: Any) -> float:
18
24
  if (
@@ -26,6 +32,18 @@ def validate_exec_timeout_seconds(value: Any) -> float:
26
32
  )
27
33
  return float(value)
28
34
 
35
+
36
+ def validate_positive_timeout_seconds(value: Any, *, label: str) -> float:
37
+ if (
38
+ isinstance(value, bool)
39
+ or not isinstance(value, (int, float))
40
+ or not math.isfinite(value)
41
+ or value <= 0
42
+ ):
43
+ raise ValueError(f"{label} must be a positive finite number")
44
+ return float(value)
45
+
46
+
29
47
  # Budget for conservative six-byte JSON escaping:
30
48
  # 8 MiB content * 6 + 64 maps * 2 paths * 4 KiB * 6 = 51 MiB.
31
49
  # The remaining 13 MiB covers the JSON-RPC envelope and structural overhead.
@@ -21,19 +21,17 @@ def _required(value: str, label: str) -> str:
21
21
  return value
22
22
 
23
23
 
24
+ _NODE_LEGACY_FIELDS = frozenset({"remote_bin", "auto_bootstrap"})
25
+
26
+
24
27
  @dataclass(frozen=True)
25
28
  class Node:
26
29
  name: str
27
30
  ssh_host: str
28
- remote_bin: str = "~/.local/bin/msdevd"
29
- auto_bootstrap: bool = True
30
31
 
31
32
  def __post_init__(self) -> None:
32
33
  _required(self.name, "node name")
33
34
  _required(self.ssh_host, "SSH host")
34
- _required(self.remote_bin, "remote binary")
35
- if type(self.auto_bootstrap) is not bool:
36
- raise ValueError("auto_bootstrap must be a boolean")
37
35
 
38
36
 
39
37
  @dataclass(frozen=True)
@@ -124,7 +122,12 @@ class _ResourceStore:
124
122
  environments: dict[str, dict[str, Any]] = {}
125
123
  try:
126
124
  for name, raw in value["nodes"].items():
127
- node = Node(**raw)
125
+ node_raw = {
126
+ key: item
127
+ for key, item in dict(raw).items()
128
+ if key not in _NODE_LEGACY_FIELDS
129
+ }
130
+ node = Node(**node_raw)
128
131
  if node.name != name:
129
132
  raise ValueError(f"node key {name!r} does not match its name")
130
133
  nodes[name] = asdict(node)
@@ -248,6 +251,47 @@ class NodeStore(_ResourceStore):
248
251
  self._save(self._normalize_saved(value))
249
252
  return references
250
253
 
254
+ def rename(self, old: str, new: str) -> Node:
255
+ """Rename a node and retarget environments that reference it.
256
+
257
+ The same-named host environment is renamed with the node. ``ssh_host``
258
+ is updated only when it was the old node name.
259
+ """
260
+ if old == "local" or new == "local":
261
+ raise ValueError("node name 'local' is reserved")
262
+ _required(old, "node name")
263
+ _required(new, "node name")
264
+ if old == new:
265
+ return self.get(old)
266
+ with _exclusive_file_lock(self.lock_path):
267
+ value = self._load_unlocked()
268
+ if old not in value["nodes"]:
269
+ raise KeyError(f"unknown node: {old}")
270
+ if new in value["nodes"]:
271
+ raise ValueError(f"node {new!r} already exists")
272
+ environments = value["environments"]
273
+ if old in environments and new in environments:
274
+ raise ValueError(f"environment {new!r} already exists")
275
+ raw = dict(value["nodes"].pop(old))
276
+ ssh_host = raw["ssh_host"]
277
+ if ssh_host == old:
278
+ ssh_host = new
279
+ node = Node(name=new, ssh_host=ssh_host)
280
+ value["nodes"][new] = asdict(node)
281
+ rewritten: dict[str, Any] = {}
282
+ for name, environment in environments.items():
283
+ updated = dict(environment)
284
+ if updated.get("node") == old:
285
+ updated["node"] = new
286
+ if name == old:
287
+ updated["name"] = new
288
+ rewritten[new] = updated
289
+ else:
290
+ rewritten[name] = updated
291
+ value["environments"] = rewritten
292
+ self._save(self._normalize_saved(value))
293
+ return node
294
+
251
295
 
252
296
  class EnvironmentStore(_ResourceStore):
253
297
  def add(self, environment: Environment, *, force: bool = False) -> None:
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass, field
6
- from pathlib import Path
7
6
  from typing import Any, Protocol
8
7
 
9
8
  from ..resources import Environment, EnvironmentStore, Node, NodeStore
@@ -24,11 +23,7 @@ class TransportFactory(Protocol):
24
23
 
25
24
 
26
25
  class BootstrapNode(Protocol):
27
- def __call__(
28
- self,
29
- node: Node,
30
- package_path: Path | None = None,
31
- ) -> dict[str, Any]: ...
26
+ def __call__(self, node: Node) -> dict[str, Any]: ...
32
27
 
33
28
 
34
29
  def create_default_transport(
@@ -189,13 +189,12 @@ class EnvironmentService:
189
189
  def open(self, name: str) -> dict[str, Any]:
190
190
  environment = self._environment(name)
191
191
  transport = self.context.transport_for_environment(environment.name)
192
- open_env = getattr(transport, "_ensure_env_open", None)
193
- if open_env is None:
194
- raise TypeError("environment transport does not support env.open")
195
- open_env()
196
- result = transport.call("env.status", {"_env": environment.to_agent_spec()})
192
+ connect = getattr(transport, "connect", None)
193
+ if connect is not None:
194
+ connect()
195
+ result = transport.call("env.open", {"_env": environment.to_agent_spec()})
197
196
  if not isinstance(result, dict):
198
- raise RuntimeError("env.status returned a non-object result")
197
+ raise RuntimeError("env.open returned a non-object result")
199
198
  return result
200
199
 
201
200
  def remove(self, name: str) -> str: