@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.
- package/README.md +24 -15
- package/bin/hostbridge.js +80 -9
- package/docs/ARCHITECTURE.md +62 -0
- package/examples/claude_desktop_config.json +1 -1
- package/examples/codex.config.toml +1 -1
- package/examples/hosts.example.json +1 -0
- package/package.json +2 -1
- package/pyproject.toml +5 -4
- package/src/server_control_mcp/__init__.py +60 -24
- package/src/server_control_mcp/async_lifecycle.py +41 -0
- package/src/server_control_mcp/cli.py +2 -2
- package/src/server_control_mcp/client.py +345 -238
- package/src/server_control_mcp/config.py +18 -6
- package/src/server_control_mcp/daemon.py +309 -412
- package/src/server_control_mcp/doctor.py +41 -5
- package/src/server_control_mcp/hosts.py +252 -24
- package/src/server_control_mcp/mock_ssh.py +97 -960
- package/src/server_control_mcp/mock_ssh_exec.py +299 -0
- package/src/server_control_mcp/mock_ssh_session.py +69 -0
- package/src/server_control_mcp/mock_ssh_sftp.py +604 -0
- package/src/server_control_mcp/mock_ssh_tunnel.py +289 -0
- 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/policy.py +86 -14
- 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/remote_task_agent.py +102 -0
- package/src/server_control_mcp/runtime.py +3 -2
- package/src/server_control_mcp/runtime_services.py +862 -0
- package/src/server_control_mcp/secrets.py +6 -6
- package/src/server_control_mcp/secure_files.py +121 -0
- package/src/server_control_mcp/server.py +53 -20
- package/src/server_control_mcp/services.py +3 -469
- 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 +315 -84
- 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 -362
- 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
|
-
|
|
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
|
|
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
|
|
112
|
-
raise ValueError(f"{source}.hosts[{index}].limits
|
|
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(
|