plato-sdk-v2 2.7.6__py3-none-any.whl → 2.7.7__py3-none-any.whl
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.
- plato/cli/__init__.py +5 -0
- plato/cli/agent.py +1209 -0
- plato/cli/audit_ui.py +316 -0
- plato/cli/chronos.py +817 -0
- plato/cli/main.py +193 -0
- plato/cli/pm.py +1206 -0
- plato/cli/proxy.py +222 -0
- plato/cli/sandbox.py +808 -0
- plato/cli/utils.py +200 -0
- plato/cli/verify.py +690 -0
- plato/cli/world.py +250 -0
- plato/v1/cli/pm.py +4 -1
- plato/v2/__init__.py +2 -0
- plato/v2/models.py +42 -0
- plato/v2/sync/__init__.py +6 -0
- plato/v2/sync/client.py +6 -3
- plato/v2/sync/sandbox.py +1461 -0
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.7.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.7.dist-info}/RECORD +21 -9
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.7.dist-info}/entry_points.txt +1 -1
- {plato_sdk_v2-2.7.6.dist-info → plato_sdk_v2-2.7.7.dist-info}/WHEEL +0 -0
plato/cli/proxy.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""SSH and gateway utilities for Plato CLI.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- SSH key generation and config file management
|
|
5
|
+
- TLS connection utilities for port forwarding via TLS + SNI gateway
|
|
6
|
+
|
|
7
|
+
The CLI commands (ssh, tunnel) are in sandbox.py.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import socket
|
|
12
|
+
import ssl
|
|
13
|
+
import subprocess
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from cryptography.hazmat.primitives import serialization
|
|
17
|
+
from cryptography.hazmat.primitives.asymmetric import ed25519
|
|
18
|
+
|
|
19
|
+
# Default gateway configuration
|
|
20
|
+
DEFAULT_GATEWAY_HOST = "gateway.plato.so"
|
|
21
|
+
DEFAULT_GATEWAY_PORT = 443
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_gateway_config() -> tuple[str, int]:
|
|
25
|
+
"""Get gateway host and port from environment or defaults."""
|
|
26
|
+
host = os.environ.get("PLATO_GATEWAY_HOST", DEFAULT_GATEWAY_HOST)
|
|
27
|
+
port = int(os.environ.get("PLATO_GATEWAY_PORT", str(DEFAULT_GATEWAY_PORT)))
|
|
28
|
+
return host, port
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_plato_dir(working_dir: Path | str | None = None) -> Path:
|
|
32
|
+
"""Get the .plato directory for config/SSH files.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
working_dir: If provided, returns working_dir/.plato.
|
|
36
|
+
If None, returns cwd/.plato (workspace-based).
|
|
37
|
+
"""
|
|
38
|
+
if working_dir is not None:
|
|
39
|
+
return Path(working_dir) / ".plato"
|
|
40
|
+
return Path.cwd() / ".plato"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def generate_ssh_key_pair(identifier: str, working_dir: Path | str | None = None) -> tuple[str, str]:
|
|
44
|
+
"""Generate a new ed25519 SSH key pair.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
identifier: A unique identifier for naming the key files (e.g., job_id prefix)
|
|
48
|
+
working_dir: Optional working directory for the .plato folder
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Tuple of (public_key_str, private_key_path)
|
|
52
|
+
"""
|
|
53
|
+
plato_dir = get_plato_dir(working_dir)
|
|
54
|
+
plato_dir.mkdir(mode=0o700, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
private_key_path = plato_dir / f"ssh_{identifier}_key"
|
|
57
|
+
public_key_path = plato_dir / f"ssh_{identifier}_key.pub"
|
|
58
|
+
|
|
59
|
+
# Remove existing keys if they exist
|
|
60
|
+
private_key_path.unlink(missing_ok=True)
|
|
61
|
+
public_key_path.unlink(missing_ok=True)
|
|
62
|
+
|
|
63
|
+
# Generate ed25519 key pair
|
|
64
|
+
private_key = ed25519.Ed25519PrivateKey.generate()
|
|
65
|
+
public_key = private_key.public_key()
|
|
66
|
+
|
|
67
|
+
# Serialize private key in OpenSSH format
|
|
68
|
+
private_key_bytes = private_key.private_bytes(
|
|
69
|
+
encoding=serialization.Encoding.PEM,
|
|
70
|
+
format=serialization.PrivateFormat.OpenSSH,
|
|
71
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Serialize public key in OpenSSH format
|
|
75
|
+
public_key_bytes = public_key.public_bytes(
|
|
76
|
+
encoding=serialization.Encoding.OpenSSH,
|
|
77
|
+
format=serialization.PublicFormat.OpenSSH,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Add comment to public key
|
|
81
|
+
comment = f"plato-sandbox-{identifier}"
|
|
82
|
+
public_key_str = f"{public_key_bytes.decode('utf-8')} {comment}"
|
|
83
|
+
|
|
84
|
+
# Write private key with 0600 permissions
|
|
85
|
+
private_key_path.write_bytes(private_key_bytes)
|
|
86
|
+
private_key_path.chmod(0o600)
|
|
87
|
+
|
|
88
|
+
# Write public key with 0644 permissions
|
|
89
|
+
public_key_path.write_text(public_key_str + "\n")
|
|
90
|
+
public_key_path.chmod(0o644)
|
|
91
|
+
|
|
92
|
+
return public_key_str, str(private_key_path)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def generate_ssh_config(
|
|
96
|
+
job_id: str,
|
|
97
|
+
private_key_path: str,
|
|
98
|
+
gateway_host: str | None = None,
|
|
99
|
+
gateway_port: int | None = None,
|
|
100
|
+
working_dir: Path | str | None = None,
|
|
101
|
+
) -> str:
|
|
102
|
+
"""Generate .plato/ssh_config file for easy SSH access.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
job_id: The job ID for the sandbox VM.
|
|
106
|
+
private_key_path: Path to the SSH private key file.
|
|
107
|
+
gateway_host: Gateway hostname (default: from env or gateway.plato.so).
|
|
108
|
+
gateway_port: Gateway port (default: from env or 443).
|
|
109
|
+
working_dir: Working directory for .plato folder.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Absolute path to the generated ssh_config file.
|
|
113
|
+
"""
|
|
114
|
+
if gateway_host is None:
|
|
115
|
+
gateway_host = os.environ.get("PLATO_GATEWAY_HOST", DEFAULT_GATEWAY_HOST)
|
|
116
|
+
if gateway_port is None:
|
|
117
|
+
gateway_port = int(os.environ.get("PLATO_GATEWAY_PORT", str(DEFAULT_GATEWAY_PORT)))
|
|
118
|
+
|
|
119
|
+
plato_dir = get_plato_dir(working_dir)
|
|
120
|
+
plato_dir.mkdir(mode=0o700, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
ssh_config_path = plato_dir / "ssh_config"
|
|
123
|
+
|
|
124
|
+
config_content = f"""# Plato SSH Config
|
|
125
|
+
# Usage: ssh -F .plato/ssh_config sandbox
|
|
126
|
+
|
|
127
|
+
Host *.plato
|
|
128
|
+
User root
|
|
129
|
+
StrictHostKeyChecking no
|
|
130
|
+
UserKnownHostsFile /dev/null
|
|
131
|
+
ProxyCommand openssl s_client -quiet -connect {gateway_host}:{gateway_port} -servername %h--22.{gateway_host} 2>/dev/null
|
|
132
|
+
|
|
133
|
+
Host sandbox
|
|
134
|
+
HostName {job_id}.plato
|
|
135
|
+
IdentityFile {private_key_path}
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
ssh_config_path.write_text(config_content)
|
|
139
|
+
ssh_config_path.chmod(0o600)
|
|
140
|
+
|
|
141
|
+
return str(ssh_config_path)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def run_ssh_command(ssh_config_path: str, ssh_host: str, command: str) -> tuple[int, str, str]:
|
|
145
|
+
"""Run a command on the remote VM via SSH.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
ssh_config_path: Path to SSH config file.
|
|
149
|
+
ssh_host: SSH host alias (e.g., "sandbox").
|
|
150
|
+
command: Shell command to execute.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Tuple of (returncode, stdout, stderr).
|
|
154
|
+
"""
|
|
155
|
+
result = subprocess.run(
|
|
156
|
+
["ssh", "-F", ssh_config_path, ssh_host, command],
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
)
|
|
160
|
+
return result.returncode, result.stdout, result.stderr
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def create_tls_connection(
|
|
164
|
+
gateway_host: str,
|
|
165
|
+
gateway_port: int,
|
|
166
|
+
sni: str,
|
|
167
|
+
verify_ssl: bool = True,
|
|
168
|
+
) -> ssl.SSLSocket:
|
|
169
|
+
"""Create a TLS connection to the gateway with the specified SNI.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
gateway_host: The gateway hostname.
|
|
173
|
+
gateway_port: The gateway port.
|
|
174
|
+
sni: The SNI (Server Name Indication) for routing.
|
|
175
|
+
verify_ssl: Whether to verify SSL certificates.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
An SSL socket connected to the gateway.
|
|
179
|
+
"""
|
|
180
|
+
# Create SSL context
|
|
181
|
+
context = ssl.create_default_context()
|
|
182
|
+
if not verify_ssl:
|
|
183
|
+
context.check_hostname = False
|
|
184
|
+
context.verify_mode = ssl.CERT_NONE
|
|
185
|
+
|
|
186
|
+
# Create socket and wrap with TLS
|
|
187
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
188
|
+
sock.settimeout(30)
|
|
189
|
+
|
|
190
|
+
# Wrap with TLS, using SNI for routing
|
|
191
|
+
ssl_sock = context.wrap_socket(sock, server_hostname=sni)
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
ssl_sock.connect((gateway_host, gateway_port))
|
|
195
|
+
except Exception as e:
|
|
196
|
+
ssl_sock.close()
|
|
197
|
+
raise ConnectionError(f"Failed to connect to gateway: {e}") from e
|
|
198
|
+
|
|
199
|
+
return ssl_sock
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def forward_data(src: socket.socket, dst: socket.socket, name: str = "") -> None:
|
|
203
|
+
"""Forward data between two sockets until one closes.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
src: Source socket to read from.
|
|
207
|
+
dst: Destination socket to write to.
|
|
208
|
+
name: Optional name for debugging.
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
while True:
|
|
212
|
+
data = src.recv(4096)
|
|
213
|
+
if not data:
|
|
214
|
+
break
|
|
215
|
+
dst.sendall(data)
|
|
216
|
+
except (ConnectionResetError, BrokenPipeError, OSError):
|
|
217
|
+
pass
|
|
218
|
+
finally:
|
|
219
|
+
try:
|
|
220
|
+
dst.shutdown(socket.SHUT_WR)
|
|
221
|
+
except OSError:
|
|
222
|
+
pass
|