@neoline/hostbridge 2.0.4 → 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 (40) hide show
  1. package/README.md +11 -9
  2. package/docs/ARCHITECTURE.md +62 -0
  3. package/package.json +2 -1
  4. package/pyproject.toml +1 -1
  5. package/src/server_control_mcp/__init__.py +4 -3
  6. package/src/server_control_mcp/async_lifecycle.py +41 -0
  7. package/src/server_control_mcp/cli.py +1 -1
  8. package/src/server_control_mcp/client.py +302 -287
  9. package/src/server_control_mcp/config.py +2 -1
  10. package/src/server_control_mcp/daemon.py +233 -526
  11. package/src/server_control_mcp/doctor.py +34 -2
  12. package/src/server_control_mcp/hosts.py +136 -2
  13. package/src/server_control_mcp/mock_ssh.py +52 -14
  14. package/src/server_control_mcp/mock_ssh_exec.py +147 -58
  15. package/src/server_control_mcp/mock_ssh_session.py +3 -2
  16. package/src/server_control_mcp/mock_ssh_sftp.py +126 -28
  17. package/src/server_control_mcp/mock_ssh_tunnel.py +141 -444
  18. package/src/server_control_mcp/mux_connection.py +326 -0
  19. package/src/server_control_mcp/mux_daemon.py +186 -0
  20. package/src/server_control_mcp/mux_protocol.py +279 -0
  21. package/src/server_control_mcp/mux_records.py +71 -0
  22. package/src/server_control_mcp/mux_rpc.py +1779 -0
  23. package/src/server_control_mcp/mux_service.py +506 -0
  24. package/src/server_control_mcp/mux_stream.py +283 -0
  25. package/src/server_control_mcp/mux_sync_client.py +224 -0
  26. package/src/server_control_mcp/remote_agent_bundle.py +56 -0
  27. package/src/server_control_mcp/remote_mux_agent.py +769 -0
  28. package/src/server_control_mcp/runtime_services.py +862 -0
  29. package/src/server_control_mcp/server.py +33 -18
  30. package/src/server_control_mcp/services.py +2 -666
  31. package/src/server_control_mcp/transports/__init__.py +4 -8
  32. package/src/server_control_mcp/transports/base.py +60 -38
  33. package/src/server_control_mcp/transports/ssh.py +206 -259
  34. package/src/server_control_mcp/tunnel_manager.py +1245 -0
  35. package/src/server_control_mcp/tunnel_native_ssh.py +454 -0
  36. package/src/server_control_mcp/tunnel_providers.py +20 -0
  37. package/src/server_control_mcp/tunnel_pty_agent.py +909 -0
  38. package/src/server_control_mcp/protocol.py +0 -363
  39. package/src/server_control_mcp/remote_tunnel_agent.py +0 -143
  40. package/src/server_control_mcp/transports/pty.py +0 -515
@@ -1,17 +1,13 @@
1
- """Remote transport implementations used by HostBridge daemon services."""
1
+ """Async transport primitives used by HostBridge host runtimes."""
2
2
 
3
- from .base import ExecResult, ShellReadResult, TransferResult, Transport, TransportBusy, TransportCapabilities
4
- from .pty import PtyTransport
5
- from .ssh import SshConnectionConfig, SshTransport
3
+ from .base import ExecResult, ShellReadResult, TransferResult, TransportCapabilities
4
+ from .ssh import AsyncSshConnection, SshConnectionConfig
6
5
 
7
6
  __all__ = [
8
7
  "ExecResult",
9
- "PtyTransport",
8
+ "AsyncSshConnection",
10
9
  "SshConnectionConfig",
11
- "SshTransport",
12
10
  "ShellReadResult",
13
11
  "TransferResult",
14
- "Transport",
15
- "TransportBusy",
16
12
  "TransportCapabilities",
17
13
  ]
@@ -2,17 +2,40 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Iterable, Iterator
5
+ import asyncio
6
+ from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator, Awaitable, Iterable, Iterator, Mapping
6
7
  from dataclasses import dataclass
7
- from typing import Protocol
8
8
 
9
9
 
10
10
  class TransportError(RuntimeError):
11
11
  """Base error raised by a remote transport."""
12
12
 
13
13
 
14
- class TransportBusy(TransportError):
15
- """Raised when an ordered transport already has an active operation."""
14
+ ByteStream = AsyncIterable[bytes] | Iterable[bytes]
15
+
16
+
17
+ async def iterate_byte_stream(chunks: ByteStream, *, label: str) -> AsyncGenerator[bytes, None]:
18
+ if isinstance(chunks, AsyncIterable):
19
+ async for chunk in chunks:
20
+ if not isinstance(chunk, bytes):
21
+ raise TypeError(f"{label} chunks must be bytes")
22
+ yield chunk
23
+ return
24
+ iterator = iter(chunks)
25
+ while True:
26
+ present, chunk = await asyncio.to_thread(_next_chunk, iterator)
27
+ if not present:
28
+ return
29
+ if not isinstance(chunk, bytes):
30
+ raise TypeError(f"{label} chunks must be bytes")
31
+ yield chunk
32
+
33
+
34
+ def _next_chunk(iterator: Iterator[bytes]) -> tuple[bool, bytes]:
35
+ try:
36
+ return True, next(iterator)
37
+ except StopIteration:
38
+ return False, b""
16
39
 
17
40
 
18
41
  @dataclass(frozen=True, slots=True)
@@ -42,37 +65,36 @@ class ShellReadResult:
42
65
  alive: bool
43
66
 
44
67
 
45
- class Transport(Protocol):
46
- capabilities: TransportCapabilities
47
-
48
- def exec(
49
- self,
50
- command: str,
51
- *,
52
- stdin: Iterable[bytes] | None,
53
- timeout: float,
54
- output_limit: int,
55
- ) -> ExecResult:
56
- raise NotImplementedError
57
-
58
- def upload(
59
- self,
60
- chunks: Iterable[bytes],
61
- remote_path: str,
62
- *,
63
- mode: int,
64
- expected_size: int | None,
65
- ) -> TransferResult:
66
- raise NotImplementedError
67
-
68
- def download(self, remote_path: str, *, chunk_size: int) -> Iterator[bytes]:
69
- raise NotImplementedError
70
-
71
- def close(self) -> None:
72
- raise NotImplementedError
73
-
74
- def shell_write(self, data: bytes) -> None:
75
- raise NotImplementedError
76
-
77
- def shell_read(self, *, timeout: float, max_bytes: int) -> ShellReadResult:
78
- raise NotImplementedError
68
+ @dataclass(frozen=True, slots=True)
69
+ class ShellTerminalResult:
70
+ exit_code: int | None
71
+ signal: str | None = None
72
+
73
+ def __post_init__(self) -> None:
74
+ if self.exit_code is not None and (
75
+ not isinstance(self.exit_code, int) or isinstance(self.exit_code, bool) or not 0 <= self.exit_code <= 255
76
+ ):
77
+ raise ValueError("shell exit_code must be between 0 and 255 or null")
78
+ if self.signal is not None and (not isinstance(self.signal, str) or not self.signal.strip()):
79
+ raise ValueError("shell signal must be a non-empty string or null")
80
+ if self.exit_code is not None and self.signal is not None:
81
+ raise ValueError("shell terminal result cannot contain both exit_code and signal")
82
+
83
+ @classmethod
84
+ def from_mapping(cls, value: Mapping[str, object]) -> ShellTerminalResult:
85
+ return cls(value.get("exit_code"), value.get("signal")) # type: ignore[arg-type]
86
+
87
+ def to_mapping(self) -> dict[str, object]:
88
+ return {"exit_code": self.exit_code, "signal": self.signal}
89
+
90
+
91
+ class ShellAttachment:
92
+ def __init__(self, output: AsyncIterator[bytes], terminal: Awaitable[ShellTerminalResult]) -> None:
93
+ self._output = output
94
+ self._terminal = terminal
95
+
96
+ def __aiter__(self) -> AsyncIterator[bytes]:
97
+ return self._output
98
+
99
+ async def finish(self) -> ShellTerminalResult:
100
+ return await self._terminal