@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.
- package/README.md +11 -9
- package/docs/ARCHITECTURE.md +62 -0
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/server_control_mcp/__init__.py +4 -3
- package/src/server_control_mcp/async_lifecycle.py +41 -0
- package/src/server_control_mcp/cli.py +1 -1
- package/src/server_control_mcp/client.py +302 -287
- package/src/server_control_mcp/config.py +2 -1
- package/src/server_control_mcp/daemon.py +233 -526
- package/src/server_control_mcp/doctor.py +34 -2
- package/src/server_control_mcp/hosts.py +136 -2
- package/src/server_control_mcp/mock_ssh.py +52 -14
- package/src/server_control_mcp/mock_ssh_exec.py +147 -58
- package/src/server_control_mcp/mock_ssh_session.py +3 -2
- package/src/server_control_mcp/mock_ssh_sftp.py +126 -28
- package/src/server_control_mcp/mock_ssh_tunnel.py +141 -444
- package/src/server_control_mcp/mux_connection.py +326 -0
- package/src/server_control_mcp/mux_daemon.py +186 -0
- package/src/server_control_mcp/mux_protocol.py +279 -0
- package/src/server_control_mcp/mux_records.py +71 -0
- package/src/server_control_mcp/mux_rpc.py +1779 -0
- package/src/server_control_mcp/mux_service.py +506 -0
- package/src/server_control_mcp/mux_stream.py +283 -0
- package/src/server_control_mcp/mux_sync_client.py +224 -0
- package/src/server_control_mcp/remote_agent_bundle.py +56 -0
- package/src/server_control_mcp/remote_mux_agent.py +769 -0
- package/src/server_control_mcp/runtime_services.py +862 -0
- package/src/server_control_mcp/server.py +33 -18
- package/src/server_control_mcp/services.py +2 -666
- package/src/server_control_mcp/transports/__init__.py +4 -8
- package/src/server_control_mcp/transports/base.py +60 -38
- package/src/server_control_mcp/transports/ssh.py +206 -259
- package/src/server_control_mcp/tunnel_manager.py +1245 -0
- package/src/server_control_mcp/tunnel_native_ssh.py +454 -0
- package/src/server_control_mcp/tunnel_providers.py +20 -0
- package/src/server_control_mcp/tunnel_pty_agent.py +909 -0
- package/src/server_control_mcp/protocol.py +0 -363
- package/src/server_control_mcp/remote_tunnel_agent.py +0 -143
- package/src/server_control_mcp/transports/pty.py +0 -515
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Async transport primitives used by HostBridge host runtimes."""
|
|
2
2
|
|
|
3
|
-
from .base import ExecResult, ShellReadResult, TransferResult,
|
|
4
|
-
from .
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|