@neoline/hostbridge 2.0.3 → 2.0.5

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 (48) hide show
  1. package/README.md +24 -15
  2. package/bin/hostbridge.js +80 -9
  3. package/docs/ARCHITECTURE.md +62 -0
  4. package/examples/claude_desktop_config.json +1 -1
  5. package/examples/codex.config.toml +1 -1
  6. package/examples/hosts.example.json +1 -0
  7. package/package.json +2 -1
  8. package/pyproject.toml +5 -4
  9. package/src/server_control_mcp/__init__.py +60 -24
  10. package/src/server_control_mcp/async_lifecycle.py +41 -0
  11. package/src/server_control_mcp/cli.py +2 -2
  12. package/src/server_control_mcp/client.py +345 -238
  13. package/src/server_control_mcp/config.py +18 -6
  14. package/src/server_control_mcp/daemon.py +309 -412
  15. package/src/server_control_mcp/doctor.py +41 -5
  16. package/src/server_control_mcp/hosts.py +252 -24
  17. package/src/server_control_mcp/mock_ssh.py +97 -960
  18. package/src/server_control_mcp/mock_ssh_exec.py +299 -0
  19. package/src/server_control_mcp/mock_ssh_session.py +69 -0
  20. package/src/server_control_mcp/mock_ssh_sftp.py +604 -0
  21. package/src/server_control_mcp/mock_ssh_tunnel.py +289 -0
  22. package/src/server_control_mcp/mux_connection.py +326 -0
  23. package/src/server_control_mcp/mux_daemon.py +186 -0
  24. package/src/server_control_mcp/mux_protocol.py +279 -0
  25. package/src/server_control_mcp/mux_records.py +71 -0
  26. package/src/server_control_mcp/mux_rpc.py +1779 -0
  27. package/src/server_control_mcp/mux_service.py +506 -0
  28. package/src/server_control_mcp/mux_stream.py +283 -0
  29. package/src/server_control_mcp/mux_sync_client.py +224 -0
  30. package/src/server_control_mcp/policy.py +86 -14
  31. package/src/server_control_mcp/remote_agent_bundle.py +56 -0
  32. package/src/server_control_mcp/remote_mux_agent.py +769 -0
  33. package/src/server_control_mcp/remote_task_agent.py +102 -0
  34. package/src/server_control_mcp/runtime.py +3 -2
  35. package/src/server_control_mcp/runtime_services.py +862 -0
  36. package/src/server_control_mcp/secrets.py +6 -6
  37. package/src/server_control_mcp/secure_files.py +121 -0
  38. package/src/server_control_mcp/server.py +53 -20
  39. package/src/server_control_mcp/services.py +3 -469
  40. package/src/server_control_mcp/transports/__init__.py +4 -8
  41. package/src/server_control_mcp/transports/base.py +60 -38
  42. package/src/server_control_mcp/transports/ssh.py +315 -84
  43. package/src/server_control_mcp/tunnel_manager.py +1245 -0
  44. package/src/server_control_mcp/tunnel_native_ssh.py +454 -0
  45. package/src/server_control_mcp/tunnel_providers.py +20 -0
  46. package/src/server_control_mcp/tunnel_pty_agent.py +909 -0
  47. package/src/server_control_mcp/protocol.py +0 -362
  48. package/src/server_control_mcp/transports/pty.py +0 -434
@@ -10,11 +10,21 @@ from dataclasses import dataclass
10
10
  from pathlib import Path
11
11
 
12
12
  from .secrets import SCHEME, encrypt_secret
13
+ from .secure_files import read_private_text
13
14
 
14
15
  SCHEMA_VERSION = 1
15
16
  _HOST_ID = re.compile(r"^[A-Za-z0-9_.-]+$")
16
17
  _CONNECTION_STYLES = {"script", "ssh", "command"}
17
18
  _TRANSPORTS = {"auto", "pty", "ssh"}
19
+ _LIMITS = {
20
+ "max_sessions",
21
+ "command_timeout_seconds",
22
+ "max_output_bytes",
23
+ "max_stdin_bytes",
24
+ "max_transfer_bytes",
25
+ "max_text_bytes",
26
+ "max_shell_bytes",
27
+ }
18
28
 
19
29
 
20
30
  @dataclass(frozen=True, slots=True)
@@ -25,14 +35,15 @@ class ConfigDocument:
25
35
  @property
26
36
  def hosts(self) -> list[dict[str, object]]:
27
37
  value = self.data["hosts"]
28
- assert isinstance(value, list)
38
+ if not isinstance(value, list):
39
+ raise ValueError("config hosts must be a list")
29
40
  return value
30
41
 
31
42
 
32
43
  def load_v1_config(path: Path | str) -> ConfigDocument:
33
44
  config_path = Path(path).expanduser()
34
45
  try:
35
- payload = json.loads(config_path.read_text(encoding="utf-8"))
46
+ payload = json.loads(read_private_text(config_path))
36
47
  except json.JSONDecodeError as exc:
37
48
  raise ValueError(f"{config_path} is not valid JSON") from exc
38
49
  validated = validate_v1_config(payload, source=config_path)
@@ -108,8 +119,10 @@ def _validate_limits(value: object, source: Path | str, index: int) -> None:
108
119
  if not isinstance(value, dict):
109
120
  raise ValueError(f"{source}.hosts[{index}].limits must be an object")
110
121
  for name, limit in value.items():
111
- if not isinstance(name, str) or not isinstance(limit, int) or isinstance(limit, bool) or limit <= 0:
112
- raise ValueError(f"{source}.hosts[{index}].limits values must be positive integers")
122
+ if name not in _LIMITS:
123
+ raise ValueError(f"{source}.hosts[{index}].limits contains unknown limit {name!r}")
124
+ if not isinstance(limit, int) or isinstance(limit, bool) or limit <= 0:
125
+ raise ValueError(f"{source}.hosts[{index}].limits.{name} must be a positive integer")
113
126
 
114
127
 
115
128
  def migrate_config(source: Path | str, destination: Path | str) -> ConfigDocument:
@@ -132,8 +145,7 @@ def migrate_config(source: Path | str, destination: Path | str) -> ConfigDocumen
132
145
  secrets = host.get("secrets")
133
146
  if isinstance(secrets, dict):
134
147
  host["secrets"] = {
135
- name: encrypt_secret(secret) if isinstance(secret, str) else secret
136
- for name, secret in secrets.items()
148
+ name: encrypt_secret(secret) if isinstance(secret, str) else secret for name, secret in secrets.items()
137
149
  }
138
150
  migrated_hosts.append(host)
139
151
  payload = validate_v1_config(