plato-sdk-v2 2.7.1__py3-none-any.whl → 2.7.3__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/v1/cli/pm.py CHANGED
@@ -544,9 +544,19 @@ def review_base(
544
544
  console.print(f"[bold red]📋 Most Recent Base Review: REJECTED[/bold red] ({timestamp})")
545
545
  else:
546
546
  console.print(f"[bold green]📋 Most Recent Base Review: PASSED[/bold green] ({timestamp})")
547
- comments = recent_review.get("comments")
548
- if comments:
549
- console.print(f"[yellow]Reviewer Comments:[/yellow] {comments}")
547
+ # Handle both old 'comments' field and new 'sim_comments' structure
548
+ sim_comments = recent_review.get("sim_comments")
549
+ if sim_comments:
550
+ console.print("[yellow]Reviewer Comments:[/yellow]")
551
+ for i, item in enumerate(sim_comments, 1):
552
+ comment_text = item.get("comment", "")
553
+ if comment_text:
554
+ console.print(f" {i}. {comment_text}")
555
+ else:
556
+ # Fallback to old comments field
557
+ comments = recent_review.get("comments")
558
+ if comments:
559
+ console.print(f"[yellow]Reviewer Comments:[/yellow] {comments}")
550
560
 
551
561
  console.print()
552
562
 
@@ -752,8 +762,6 @@ def review_data(
752
762
  simulator, artifact, require_artifact=False, command_name="review data"
753
763
  )
754
764
 
755
- # Determine target URL based on simulator
756
- target_url = f"https://{simulator_name}.web.plato.so"
757
765
  console.print(f"[cyan]Simulator:[/cyan] {simulator_name}")
758
766
 
759
767
  # Fetch simulator config and get artifact ID if not provided
@@ -802,21 +810,21 @@ def review_data(
802
810
  is_installed = "site-packages" in str(package_dir)
803
811
 
804
812
  if is_installed:
805
- extension_source_path = package_dir / "extensions" / "envgen-recorder-old"
813
+ extension_source_path = package_dir / "extensions" / "data-review"
806
814
  else:
807
815
  repo_root = package_dir.parent.parent.parent # plato-client/
808
- extension_source_path = repo_root / "extensions" / "envgen-recorder-old"
816
+ extension_source_path = repo_root / "extensions" / "data-review"
809
817
 
810
818
  # Fallback to env var
811
819
  if not extension_source_path.exists():
812
820
  plato_client_dir_env = os.getenv("PLATO_CLIENT_DIR")
813
821
  if plato_client_dir_env:
814
- env_path = Path(plato_client_dir_env) / "extensions" / "envgen-recorder-old"
822
+ env_path = Path(plato_client_dir_env) / "extensions" / "data-review"
815
823
  if env_path.exists():
816
824
  extension_source_path = env_path
817
825
 
818
826
  if not extension_source_path.exists():
819
- console.print("[red]❌ EnvGen Recorder extension not found[/red]")
827
+ console.print("[red]❌ Data Review extension not found[/red]")
820
828
  console.print(f"\n[yellow]Expected location:[/yellow] {extension_source_path}")
821
829
  raise typer.Exit(1)
822
830
 
@@ -829,14 +837,44 @@ def review_data(
829
837
  console.print(f"[green]✅ Extension copied to: {extension_path}[/green]")
830
838
 
831
839
  async def _review_data():
840
+ base_url = _get_base_url()
841
+ plato = AsyncPlato(api_key=api_key, base_url=base_url)
842
+ session = None
832
843
  playwright = None
833
844
  browser = None
834
845
 
835
846
  try:
847
+ # Check if we have an artifact ID to create a session
848
+ if not artifact_id:
849
+ console.print("[red]❌ No artifact ID available. Cannot create session.[/red]")
850
+ console.print("[yellow]Specify artifact with: plato pm review data -s simulator:artifact_id[/yellow]")
851
+ raise typer.Exit(1)
852
+
853
+ # Create session with artifact
854
+ console.print(f"[cyan]Creating {simulator_name} environment with artifact {artifact_id}...[/cyan]")
855
+ session = await plato.sessions.create(
856
+ envs=[Env.artifact(artifact_id)],
857
+ timeout=300,
858
+ )
859
+ console.print(f"[green]✅ Session created: {session.session_id}[/green]")
860
+
861
+ # Reset environment
862
+ console.print("[cyan]Resetting environment...[/cyan]")
863
+ await session.reset()
864
+ console.print("[green]✅ Environment reset complete![/green]")
865
+
866
+ # Get public URL
867
+ public_urls = await session.get_public_url()
868
+ first_alias = session.envs[0].alias if session.envs else None
869
+ public_url = public_urls.get(first_alias) if first_alias else None
870
+ if not public_url and public_urls:
871
+ public_url = list(public_urls.values())[0]
872
+ console.print(f"[cyan]Public URL:[/cyan] {public_url}")
873
+
836
874
  user_data_dir = Path.home() / ".plato" / "chrome-data"
837
875
  user_data_dir.mkdir(parents=True, exist_ok=True)
838
876
 
839
- console.print("[cyan]Launching Chrome with EnvGen Recorder extension...[/cyan]")
877
+ console.print("[cyan]Launching Chrome with Data Review extension...[/cyan]")
840
878
 
841
879
  from playwright.async_api import async_playwright
842
880
 
@@ -877,13 +915,14 @@ def review_data(
877
915
  else:
878
916
  console.print("[yellow]⚠️ Could not find extension ID. Please set API key manually.[/yellow]")
879
917
 
880
- # Step 1: Navigate to target URL first
881
- console.print(f"[cyan]Navigating to {target_url}...[/cyan]")
918
+ # Navigate to public URL (user logs in manually with displayed credentials)
919
+ console.print("[cyan]Opening environment...[/cyan]")
882
920
  main_page = await browser.new_page()
883
- await main_page.goto(target_url, wait_until="domcontentloaded")
884
- console.print(f"[green]✅ Loaded: {target_url}[/green]")
921
+ if public_url:
922
+ await main_page.goto(public_url)
923
+ console.print(f"[green]✅ Loaded: {public_url}[/green]")
885
924
 
886
- # Step 2: Use options page to set API key
925
+ # Use options page to set API key
887
926
  if extension_id:
888
927
  options_page = await browser.new_page()
889
928
  try:
@@ -907,17 +946,16 @@ def review_data(
907
946
  await options_page.close()
908
947
 
909
948
  # Bring main page to front
910
- await main_page.bring_to_front()
949
+ if main_page:
950
+ await main_page.bring_to_front()
911
951
 
912
952
  console.print()
913
953
  console.print("[bold]Instructions:[/bold]")
914
- console.print(" 1. Click the EnvGen Recorder extension icon to open the sidebar")
915
- if simulator:
916
- console.print(f" 2. Click 'Configure Session' and enter '{simulator}' as the simulator name")
917
- else:
918
- console.print(" 2. Click 'Configure Session' and enter a simulator name")
919
- console.print(" 3. Use the extension to record and submit reviews")
920
- console.print(" 4. When done, press Control-C to exit")
954
+ console.print(" 1. Click the Data Review extension icon to open the sidebar")
955
+ console.print(f" 2. Enter '{simulator_name}' as the simulator name and click Start Review")
956
+ console.print(" 3. Take screenshots and add comments for any issues")
957
+ console.print(" 4. Select Pass or Reject and submit the review")
958
+ console.print(" 5. When done, press Control-C to exit")
921
959
 
922
960
  # Show recent review if available
923
961
  if recent_review:
@@ -930,10 +968,20 @@ def review_data(
930
968
  else:
931
969
  console.print(f"[bold green]📋 Most Recent Data Review: PASSED[/bold green] ({timestamp})")
932
970
 
933
- comments = recent_review.get("comments")
934
- if comments:
971
+ # Handle both old 'comments' field and new 'sim_comments' structure
972
+ sim_comments = recent_review.get("sim_comments")
973
+ if sim_comments:
935
974
  console.print("\n[yellow]Reviewer Comments:[/yellow]")
936
- console.print(f" {comments}")
975
+ for i, item in enumerate(sim_comments, 1):
976
+ comment_text = item.get("comment", "")
977
+ if comment_text:
978
+ console.print(f" {i}. {comment_text}")
979
+ else:
980
+ # Fallback to old comments field
981
+ comments = recent_review.get("comments")
982
+ if comments:
983
+ console.print("\n[yellow]Reviewer Comments:[/yellow]")
984
+ console.print(f" {comments}")
937
985
  console.print("=" * 60)
938
986
 
939
987
  console.print()
@@ -954,6 +1002,8 @@ def review_data(
954
1002
 
955
1003
  finally:
956
1004
  try:
1005
+ if session:
1006
+ await session.close()
957
1007
  if browser:
958
1008
  await browser.close()
959
1009
  if playwright:
@@ -961,7 +1011,7 @@ def review_data(
961
1011
  if temp_ext_dir.exists():
962
1012
  shutil.rmtree(temp_ext_dir, ignore_errors=True)
963
1013
  except Exception as e:
964
- console.print(f"[yellow]⚠️ Browser cleanup error: {e}[/yellow]")
1014
+ console.print(f"[yellow]⚠️ Cleanup error: {e}[/yellow]")
965
1015
 
966
1016
  handle_async(_review_data())
967
1017
 
@@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
12
12
 
13
13
  from plato._generated.api.v1.simulator import get_db_config
14
14
  from plato._generated.models import DbConfigResponse
15
+ from plato.v2.utils.gateway_tunnel import GatewayTunnel, find_free_port
15
16
  from plato.v2.utils.models import (
16
17
  ApiCleanupResult,
17
18
  DatabaseCleanupResult,
@@ -19,12 +20,10 @@ from plato.v2.utils.models import (
19
20
  EnvironmentInfo,
20
21
  SessionCleanupResult,
21
22
  )
22
- from plato.v2.utils.proxy_tunnel import ProxyTunnel, find_free_port, make_db_url
23
+ from plato.v2.utils.proxy_tunnel import make_db_url
23
24
 
24
25
  logger = logging.getLogger(__name__)
25
26
 
26
- TEMP_PASSWORD = "newpass"
27
-
28
27
 
29
28
  class DatabaseCleaner:
30
29
  """Handles database audit_log cleanup operations."""
@@ -145,14 +144,13 @@ class DatabaseCleaner:
145
144
  config: DbConfigResponse,
146
145
  local_port: int,
147
146
  ) -> DatabaseCleanupResult:
148
- """Connect to a single DB via tunnel and truncate audit_log tables."""
147
+ """Connect to a single DB via gateway tunnel and truncate audit_log tables."""
149
148
  db_port = config.db_port
150
149
 
151
- tunnel = ProxyTunnel(
152
- env_id=job_id,
153
- db_port=db_port,
154
- temp_password=TEMP_PASSWORD,
155
- host_port=local_port,
150
+ tunnel = GatewayTunnel(
151
+ job_id=job_id,
152
+ remote_port=db_port,
153
+ local_port=local_port,
156
154
  )
157
155
 
158
156
  try:
@@ -0,0 +1,221 @@
1
+ """TLS + SNI gateway tunnel for database connections.
2
+
3
+ Routes traffic through gateway.plato.so:443 using SNI-based routing,
4
+ replacing the deprecated HTTP CONNECT proxy approach.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ import socket
13
+ import ssl
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Default gateway configuration
18
+ DEFAULT_GATEWAY_HOST = "gateway.plato.so"
19
+ DEFAULT_GATEWAY_PORT = 443
20
+
21
+
22
+ def get_gateway_config() -> tuple[str, int]:
23
+ """Get gateway host and port from environment or defaults.
24
+
25
+ Returns:
26
+ Tuple of (host, port) for the gateway.
27
+ """
28
+ host = os.environ.get("PLATO_GATEWAY_HOST", DEFAULT_GATEWAY_HOST)
29
+ port = int(os.environ.get("PLATO_GATEWAY_PORT", str(DEFAULT_GATEWAY_PORT)))
30
+ return host, port
31
+
32
+
33
+ def find_free_port(start_port: int = 55432) -> int:
34
+ """Find the first available TCP port starting from start_port."""
35
+ port = start_port
36
+ while port < 65535:
37
+ try:
38
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
39
+ s.bind(("127.0.0.1", port))
40
+ return port
41
+ except OSError:
42
+ port += 1
43
+ raise RuntimeError(f"No free port found starting from {start_port}")
44
+
45
+
46
+ class GatewayTunnel:
47
+ """Async TLS + SNI gateway tunnel for database connections.
48
+
49
+ Routes local connections through gateway.plato.so using SNI-based routing.
50
+ This replaces the deprecated HTTP CONNECT proxy tunnel.
51
+
52
+ Usage:
53
+ tunnel = GatewayTunnel(job_id="abc123", remote_port=5432, local_port=55432)
54
+ await tunnel.start()
55
+ # Connect to localhost:55432 to reach the VM's port 5432
56
+ await tunnel.stop()
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ job_id: str,
62
+ remote_port: int,
63
+ local_port: int,
64
+ gateway_host: str | None = None,
65
+ gateway_port: int | None = None,
66
+ verify_ssl: bool = True,
67
+ ):
68
+ """Initialize the gateway tunnel.
69
+
70
+ Args:
71
+ job_id: The job/environment ID to connect to.
72
+ remote_port: Port on the VM to forward to.
73
+ local_port: Local port to listen on.
74
+ gateway_host: Gateway hostname (default: from env or gateway.plato.so).
75
+ gateway_port: Gateway port (default: from env or 443).
76
+ verify_ssl: Whether to verify SSL certificates.
77
+ """
78
+ self.job_id = job_id
79
+ self.remote_port = remote_port
80
+ self.local_port = local_port
81
+ self.verify_ssl = verify_ssl
82
+
83
+ # Get gateway config
84
+ default_host, default_port = get_gateway_config()
85
+ self.gateway_host = gateway_host or default_host
86
+ self.gateway_port = gateway_port or default_port
87
+
88
+ # SNI for routing: {job_id}--{port}.gateway.plato.so
89
+ self.sni = f"{job_id}--{remote_port}.{self.gateway_host}"
90
+
91
+ self._server: asyncio.AbstractServer | None = None
92
+ self._client_tasks: set[asyncio.Task] = set()
93
+
94
+ async def _open_gateway_connection(
95
+ self,
96
+ timeout: float = 30.0,
97
+ ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
98
+ """Open a TLS connection to the gateway with SNI routing.
99
+
100
+ Returns:
101
+ Tuple of (reader, writer) for the gateway connection.
102
+ """
103
+ # Create SSL context
104
+ ssl_ctx = ssl.create_default_context()
105
+ if not self.verify_ssl:
106
+ ssl_ctx.check_hostname = False
107
+ ssl_ctx.verify_mode = ssl.CERT_NONE
108
+
109
+ # Connect with TLS, using SNI for routing
110
+ reader, writer = await asyncio.wait_for(
111
+ asyncio.open_connection(
112
+ self.gateway_host,
113
+ self.gateway_port,
114
+ ssl=ssl_ctx,
115
+ server_hostname=self.sni, # SNI determines which VM/port to route to
116
+ ),
117
+ timeout=timeout,
118
+ )
119
+
120
+ # Enable TCP keepalive
121
+ sock = writer.get_extra_info("socket")
122
+ if isinstance(sock, socket.socket):
123
+ try:
124
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
125
+ # macOS/BSD keepalive idle time
126
+ TCP_KEEPALIVE = getattr(socket, "TCP_KEEPALIVE", 0x10)
127
+ sock.setsockopt(socket.IPPROTO_TCP, TCP_KEEPALIVE, 30)
128
+ except OSError:
129
+ pass # Best effort
130
+
131
+ return reader, writer
132
+
133
+ async def _pipe(
134
+ self,
135
+ src: asyncio.StreamReader,
136
+ dst: asyncio.StreamWriter,
137
+ ) -> None:
138
+ """Forward data from src to dst until EOF."""
139
+ try:
140
+ while True:
141
+ data = await src.read(65536)
142
+ if not data:
143
+ break
144
+ dst.write(data)
145
+ await dst.drain()
146
+ except (ConnectionResetError, BrokenPipeError, OSError):
147
+ pass
148
+ finally:
149
+ try:
150
+ dst.close()
151
+ await dst.wait_closed()
152
+ except Exception:
153
+ pass
154
+
155
+ async def _handle_client(
156
+ self,
157
+ client_reader: asyncio.StreamReader,
158
+ client_writer: asyncio.StreamWriter,
159
+ ) -> None:
160
+ """Handle a single client connection by forwarding through gateway."""
161
+ task = asyncio.current_task()
162
+ if task:
163
+ self._client_tasks.add(task)
164
+
165
+ try:
166
+ # Connect to gateway via TLS with SNI
167
+ gateway_reader, gateway_writer = await self._open_gateway_connection()
168
+
169
+ # Bidirectional forwarding
170
+ await asyncio.gather(
171
+ self._pipe(client_reader, gateway_writer),
172
+ self._pipe(gateway_reader, client_writer),
173
+ )
174
+ except Exception as e:
175
+ logger.warning(f"Gateway tunnel error: {e}")
176
+ try:
177
+ client_writer.close()
178
+ await client_writer.wait_closed()
179
+ except Exception:
180
+ pass
181
+ finally:
182
+ if task:
183
+ self._client_tasks.discard(task)
184
+
185
+ async def start(self) -> None:
186
+ """Start the gateway tunnel server."""
187
+ logger.info(
188
+ f"Starting gateway tunnel: localhost:{self.local_port} -> "
189
+ f"{self.job_id}:{self.remote_port} via {self.gateway_host}"
190
+ )
191
+
192
+ self._server = await asyncio.start_server(
193
+ self._handle_client,
194
+ host="127.0.0.1",
195
+ port=self.local_port,
196
+ )
197
+
198
+ # Small delay to ensure binding is settled
199
+ await asyncio.sleep(0.1)
200
+
201
+ if not self._server.sockets:
202
+ raise RuntimeError("Gateway tunnel failed to start: no listening sockets")
203
+
204
+ logger.info(f"Gateway tunnel established on port {self.local_port}")
205
+
206
+ async def stop(self) -> None:
207
+ """Stop the gateway tunnel server."""
208
+ if self._server is not None:
209
+ logger.info("Stopping gateway tunnel")
210
+
211
+ # Stop accepting new connections
212
+ self._server.close()
213
+ await self._server.wait_closed()
214
+ self._server = None
215
+
216
+ # Cancel active client tasks
217
+ for t in list(self._client_tasks):
218
+ t.cancel()
219
+ self._client_tasks.clear()
220
+
221
+ logger.info("Gateway tunnel stopped")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plato-sdk-v2
3
- Version: 2.7.1
3
+ Version: 2.7.3
4
4
  Summary: Python SDK for the Plato API
5
5
  Author-email: Plato <support@plato.so>
6
6
  License-Expression: MIT
@@ -425,7 +425,7 @@ plato/v1/cli/__init__.py,sha256=om4b7PxgsoI7rEwuQelmQkqPdhMVn53_5qEN8kvksYw,105
425
425
  plato/v1/cli/agent.py,sha256=r5Eh2e2-rUIGjK5uevnGKqScABtFK-Spomrrytj-3og,44053
426
426
  plato/v1/cli/chronos.py,sha256=lzFY0nomP1AY14i8oc8OvWOdq9ydCiE3dN2XrSupvA4,27827
427
427
  plato/v1/cli/main.py,sha256=Yqy1vn4sGyAWKNpDVcLl9pbzkMn89tYVBIxFU30ZtPk,6905
428
- plato/v1/cli/pm.py,sha256=zrWGwYRC4e0d_KjOLNOqVcnJVMgS_Iw5vJ2F61jLX5s,52921
428
+ plato/v1/cli/pm.py,sha256=Q6HFTb8ZO_aB0EtAO6-OOsnVv3SoC8hL7UHpawBz46Y,55520
429
429
  plato/v1/cli/proxy.py,sha256=WmCt0R9Gos1q0FZTQSsbloNC3-Cnx6Yb60RZF1BzC18,12178
430
430
  plato/v1/cli/sandbox.py,sha256=SQb5XCdYvTHEyZxOv9ECtafTdkxpjfq45pYd-m1z7k0,101506
431
431
  plato/v1/cli/ssh.py,sha256=9ypjn5kQuaTcVjsWMDIUDyehXRH9fauk_z-C3mXzYJ8,2381
@@ -494,7 +494,8 @@ plato/v2/sync/environment.py,sha256=WnDzbyEHpwCSEP8XnfNSjIYS7rt7lYR4HGJjzprZmTQ,
494
494
  plato/v2/sync/flow_executor.py,sha256=N41-WCWIJVcCR2UmPUEiK7roNacYoeONkRXpR7lUgT8,13941
495
495
  plato/v2/sync/session.py,sha256=okXqF-CjMmA82WRy2zPXaGidbovgjAENSqiuvE4_jKE,30420
496
496
  plato/v2/utils/__init__.py,sha256=XLeFFsjXkm9g2raMmo7Wt4QN4hhCrNZDJKnpffJ4LtM,38
497
- plato/v2/utils/db_cleanup.py,sha256=lnI5lsMHNHpG85Y99MaE4Rzc3618piuzhvH-uXO1zIc,8702
497
+ plato/v2/utils/db_cleanup.py,sha256=JMzAAJz0ZnoUXtd8F4jpQmBpJpos2__RkgN_cuEearg,8692
498
+ plato/v2/utils/gateway_tunnel.py,sha256=eWgwf4VV8-jx6iCuHFgCISsAOVmNOOjCB56EuZLsnOA,7171
498
499
  plato/v2/utils/models.py,sha256=PwehSSnIRG-tM3tWL1PzZEH77ZHhIAZ9R0UPs6YknbM,1441
499
500
  plato/v2/utils/proxy_tunnel.py,sha256=8ZTd0jCGSfIHMvSv1fgEyacuISWnGPHLPbDglWroTzY,10463
500
501
  plato/worlds/README.md,sha256=XFOkEA3cNNcrWkk-Cxnsl-zn-y0kvUENKQRSqFKpdqw,5479
@@ -503,7 +504,7 @@ plato/worlds/base.py,sha256=-RR71bSxEFI5yydtrtq-AAbuw98CIjvmrbztqzB9oIc,31041
503
504
  plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
504
505
  plato/worlds/config.py,sha256=O1lUXzxp-Z_M7izslT8naXgE6XujjzwYFFrDDzUOueI,12736
505
506
  plato/worlds/runner.py,sha256=r9B2BxBae8_dM7y5cJf9xhThp_I1Qvf_tlPq2rs8qC8,4013
506
- plato_sdk_v2-2.7.1.dist-info/METADATA,sha256=W64dXq4E_YTbyTp5SBJJBm3sxSryOHQSB6oXU8x5_mI,8652
507
- plato_sdk_v2-2.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
508
- plato_sdk_v2-2.7.1.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
509
- plato_sdk_v2-2.7.1.dist-info/RECORD,,
507
+ plato_sdk_v2-2.7.3.dist-info/METADATA,sha256=XmAi4ZSPth7U15myiaNzNL98AIPMnCJjtkTsWWXMUjI,8652
508
+ plato_sdk_v2-2.7.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
509
+ plato_sdk_v2-2.7.3.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
510
+ plato_sdk_v2-2.7.3.dist-info/RECORD,,