plato-sdk-v2 2.6.1__py3-none-any.whl → 2.7.0__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/_generated/__init__.py +1 -1
- plato/_generated/api/v2/__init__.py +2 -1
- plato/_generated/api/v2/networks/__init__.py +23 -0
- plato/_generated/api/v2/networks/add_member.py +75 -0
- plato/_generated/api/v2/networks/create_network.py +70 -0
- plato/_generated/api/v2/networks/delete_network.py +68 -0
- plato/_generated/api/v2/networks/get_network.py +69 -0
- plato/_generated/api/v2/networks/list_members.py +69 -0
- plato/_generated/api/v2/networks/list_networks.py +74 -0
- plato/_generated/api/v2/networks/remove_member.py +73 -0
- plato/_generated/api/v2/networks/update_member.py +80 -0
- plato/_generated/api/v2/sessions/__init__.py +4 -0
- plato/_generated/api/v2/sessions/add_ssh_key.py +81 -0
- plato/_generated/api/v2/sessions/connect_network.py +89 -0
- plato/_generated/models/__init__.py +150 -24
- plato/v1/cli/agent.py +45 -52
- plato/v1/cli/chronos.py +46 -58
- plato/v1/cli/main.py +14 -25
- plato/v1/cli/pm.py +37 -92
- plato/v1/cli/proxy.py +343 -0
- plato/v1/cli/sandbox.py +305 -385
- plato/v1/cli/ssh.py +12 -167
- plato/v1/cli/verify.py +79 -55
- plato/v1/cli/world.py +13 -12
- plato/v2/async_/client.py +24 -2
- plato/v2/async_/session.py +48 -0
- plato/v2/sync/client.py +24 -2
- plato/v2/sync/session.py +48 -0
- {plato_sdk_v2-2.6.1.dist-info → plato_sdk_v2-2.7.0.dist-info}/METADATA +1 -1
- {plato_sdk_v2-2.6.1.dist-info → plato_sdk_v2-2.7.0.dist-info}/RECORD +32 -20
- {plato_sdk_v2-2.6.1.dist-info → plato_sdk_v2-2.7.0.dist-info}/WHEEL +0 -0
- {plato_sdk_v2-2.6.1.dist-info → plato_sdk_v2-2.7.0.dist-info}/entry_points.txt +0 -0
plato/v1/cli/sandbox.py
CHANGED
|
@@ -25,12 +25,18 @@ from plato._generated.api.v1.gitea import (
|
|
|
25
25
|
get_gitea_credentials,
|
|
26
26
|
get_simulator_repository,
|
|
27
27
|
)
|
|
28
|
-
from plato._generated.api.v1.sandbox import
|
|
28
|
+
from plato._generated.api.v1.sandbox import start_worker
|
|
29
29
|
from plato._generated.api.v2.jobs import get_flows as jobs_get_flows
|
|
30
30
|
from plato._generated.api.v2.jobs import state as jobs_state
|
|
31
|
+
from plato._generated.api.v2.sessions import (
|
|
32
|
+
add_ssh_key as sessions_add_ssh_key,
|
|
33
|
+
)
|
|
31
34
|
from plato._generated.api.v2.sessions import (
|
|
32
35
|
close as sessions_close,
|
|
33
36
|
)
|
|
37
|
+
from plato._generated.api.v2.sessions import (
|
|
38
|
+
connect_network as sessions_connect_network,
|
|
39
|
+
)
|
|
34
40
|
from plato._generated.api.v2.sessions import (
|
|
35
41
|
execute as sessions_execute,
|
|
36
42
|
)
|
|
@@ -47,17 +53,19 @@ from plato._generated.api.v2.sessions import (
|
|
|
47
53
|
state as sessions_state,
|
|
48
54
|
)
|
|
49
55
|
from plato._generated.models import (
|
|
50
|
-
|
|
56
|
+
AddSSHKeyRequest,
|
|
51
57
|
AppSchemasBuildModelsSimConfigCompute,
|
|
52
58
|
AppSchemasBuildModelsSimConfigDataset,
|
|
53
59
|
AppSchemasBuildModelsSimConfigMetadata,
|
|
60
|
+
ConnectNetworkRequest,
|
|
54
61
|
CreateCheckpointRequest,
|
|
55
62
|
ExecuteCommandRequest,
|
|
56
63
|
Flow,
|
|
57
|
-
SetupRootPasswordRequest,
|
|
58
64
|
VMManagementRequest,
|
|
59
65
|
)
|
|
60
|
-
from plato.v1.cli.
|
|
66
|
+
from plato.v1.cli.proxy import ssh as gateway_ssh_command
|
|
67
|
+
from plato.v1.cli.proxy import tunnel as gateway_tunnel_command
|
|
68
|
+
from plato.v1.cli.ssh import generate_ssh_key_pair
|
|
61
69
|
from plato.v1.cli.utils import (
|
|
62
70
|
SANDBOX_FILE,
|
|
63
71
|
console,
|
|
@@ -81,6 +89,10 @@ UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-
|
|
|
81
89
|
sandbox_app = typer.Typer(help="Manage sandboxes for simulator development")
|
|
82
90
|
sandbox_app.add_typer(sandbox_verify_app, name="verify")
|
|
83
91
|
|
|
92
|
+
# Register gateway SSH/tunnel commands
|
|
93
|
+
sandbox_app.command(name="ssh")(gateway_ssh_command)
|
|
94
|
+
sandbox_app.command(name="tunnel")(gateway_tunnel_command)
|
|
95
|
+
|
|
84
96
|
|
|
85
97
|
def format_public_url_with_router_target(public_url: str | None, service_name: str | None) -> str | None:
|
|
86
98
|
"""Format public URL with _plato_router_target parameter for browser access.
|
|
@@ -130,41 +142,45 @@ def sandbox_start(
|
|
|
130
142
|
# Common options
|
|
131
143
|
timeout: int = typer.Option(1800, "--timeout", help="VM lifetime in seconds (default: 30 minutes)"),
|
|
132
144
|
no_reset: bool = typer.Option(False, "--no-reset", help="Skip initial reset after ready"),
|
|
145
|
+
connect_network: bool = typer.Option(
|
|
146
|
+
True, "--network/--no-network", help="Connect VMs to WireGuard network for SSH access (default: enabled)"
|
|
147
|
+
),
|
|
133
148
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
134
149
|
working_dir: Path = typer.Option(
|
|
135
150
|
None, "--working-dir", "-w", help="Working directory for .sandbox.yaml and .plato/"
|
|
136
151
|
),
|
|
137
152
|
):
|
|
138
|
-
"""
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
153
|
+
"""Start a sandbox environment for simulator development.
|
|
154
|
+
|
|
155
|
+
Creates a VM that can be used to develop and test simulators. You must pick exactly
|
|
156
|
+
one mode to specify how the sandbox should be created.
|
|
157
|
+
|
|
158
|
+
Mode Options (pick exactly one):
|
|
159
|
+
-c, --from-config: Create VM using settings from plato-config.yml in the current
|
|
160
|
+
directory. Uses the compute specs (cpus, memory, disk) from the config file.
|
|
161
|
+
-s, --simulator: Start from an existing simulator. Supports formats:
|
|
162
|
+
'-s name' (latest tag), '-s name:tag' (specific tag), '-s name:uuid' (specific artifact)
|
|
163
|
+
-a, --artifact-id: Start directly from a specific artifact UUID
|
|
164
|
+
-b, --blank: Create a blank VM with custom specs (requires --service)
|
|
165
|
+
|
|
166
|
+
Config Mode Options:
|
|
167
|
+
-d, --dataset: Which dataset from the config to use (default: "base")
|
|
168
|
+
|
|
169
|
+
Simulator Mode Options:
|
|
170
|
+
-t, --tag: Artifact tag to use (default: "latest")
|
|
171
|
+
|
|
172
|
+
Blank VM Options:
|
|
173
|
+
--service: Service name for the blank VM (required with -b)
|
|
174
|
+
--cpus: Number of CPUs (default: 2)
|
|
175
|
+
--memory: Memory in MB (default: 1024)
|
|
176
|
+
--disk: Disk size in MB (default: 10240)
|
|
177
|
+
|
|
178
|
+
Common Options:
|
|
179
|
+
--timeout: VM lifetime in seconds before auto-shutdown (default: 1800 = 30 min)
|
|
180
|
+
--no-reset: Skip the initial environment reset after the VM is ready
|
|
181
|
+
--no-network: Disable WireGuard network connection (enabled by default for SSH access)
|
|
182
|
+
-j, --json: Output results as JSON instead of formatted text
|
|
183
|
+
-w, --working-dir: Directory to store .sandbox.yaml and .plato/ files
|
|
168
184
|
"""
|
|
169
185
|
api_key = require_api_key()
|
|
170
186
|
|
|
@@ -328,12 +344,17 @@ def sandbox_start(
|
|
|
328
344
|
# Create session using v2 SDK
|
|
329
345
|
if not json_output:
|
|
330
346
|
console.print("[cyan]Creating sandbox...[/cyan]")
|
|
347
|
+
if connect_network:
|
|
348
|
+
console.print("[cyan]Network connection will be established after VM is ready...[/cyan]")
|
|
349
|
+
console.print(
|
|
350
|
+
"[yellow]Note: First connection on older VMs may take a few minutes to install WireGuard[/yellow]"
|
|
351
|
+
)
|
|
331
352
|
|
|
332
353
|
try:
|
|
333
354
|
plato = PlatoV2(api_key=api_key)
|
|
334
355
|
if not env_config:
|
|
335
356
|
raise ValueError("No environment configuration provided")
|
|
336
|
-
session = plato.sessions.create(envs=[env_config], timeout=timeout)
|
|
357
|
+
session = plato.sessions.create(envs=[env_config], timeout=timeout, connect_network=connect_network)
|
|
337
358
|
|
|
338
359
|
# Get session info
|
|
339
360
|
session_id = session.session_id
|
|
@@ -363,102 +384,64 @@ def sandbox_start(
|
|
|
363
384
|
session.reset()
|
|
364
385
|
|
|
365
386
|
# Setup SSH for ALL modes (so you can SSH into any sandbox)
|
|
366
|
-
ssh_host = None
|
|
367
|
-
ssh_config_path = None
|
|
368
387
|
ssh_private_key_path = None
|
|
369
388
|
|
|
370
|
-
if
|
|
389
|
+
if session_id and connect_network:
|
|
371
390
|
if not json_output:
|
|
372
391
|
console.print("[cyan]Setting up SSH access...[/cyan]")
|
|
373
392
|
try:
|
|
374
|
-
# Step 1: Generate SSH key pair
|
|
375
|
-
# For config mode: use "plato" user (setup_sandbox configures this)
|
|
376
|
-
# For artifact/simulator modes: use "root" user (setup_root_access configures this)
|
|
377
|
-
ssh_username = "plato" if (mode == "config" and full_dataset_config_dict) else "root"
|
|
378
|
-
|
|
393
|
+
# Step 1: Generate SSH key pair
|
|
379
394
|
if not json_output:
|
|
380
395
|
console.print("[cyan] Generating SSH key pair...[/cyan]")
|
|
381
396
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
ssh_host = ssh_info["ssh_host"]
|
|
385
|
-
ssh_config_path = ssh_info["config_path"]
|
|
386
|
-
ssh_private_key_path = ssh_info["private_key_path"]
|
|
387
|
-
ssh_public_key = ssh_info["public_key"]
|
|
397
|
+
public_key, private_key_path = generate_ssh_key_pair(session_id[:8], working_dir)
|
|
398
|
+
ssh_private_key_path = private_key_path
|
|
388
399
|
|
|
400
|
+
# Step 2: Add SSH key to all VMs in the session via API
|
|
389
401
|
if not json_output:
|
|
390
|
-
console.print(
|
|
402
|
+
console.print("[cyan] Adding SSH key to VMs...[/cyan]")
|
|
391
403
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if mode == "config" and full_dataset_config_dict:
|
|
399
|
-
# Full config from plato-config.yml - use setup_sandbox API
|
|
400
|
-
compute_dict = full_dataset_config_dict.get("compute", {})
|
|
401
|
-
metadata_dict = full_dataset_config_dict.get("metadata", {})
|
|
402
|
-
services_dict = full_dataset_config_dict.get("services")
|
|
403
|
-
listeners_dict = full_dataset_config_dict.get("listeners")
|
|
404
|
-
|
|
405
|
-
compute_obj = AppSchemasBuildModelsSimConfigCompute(
|
|
406
|
-
cpus=compute_dict.get("cpus", 2),
|
|
407
|
-
memory=compute_dict.get("memory", 2048),
|
|
408
|
-
disk=compute_dict.get("disk", 10240),
|
|
409
|
-
app_port=compute_dict.get("app_port", 80),
|
|
410
|
-
plato_messaging_port=compute_dict.get("plato_messaging_port", 7000),
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
metadata_obj = AppSchemasBuildModelsSimConfigMetadata(
|
|
414
|
-
name=metadata_dict.get("name", sim_name or "sandbox"),
|
|
415
|
-
description=metadata_dict.get("description", ""),
|
|
416
|
-
source_code_url=metadata_dict.get("source_code_url"),
|
|
417
|
-
start_url=metadata_dict.get("start_url", "blank"),
|
|
418
|
-
license=metadata_dict.get("license"),
|
|
419
|
-
variables=metadata_dict.get("variables"),
|
|
420
|
-
flows_path=metadata_dict.get("flows_path"),
|
|
421
|
-
)
|
|
404
|
+
ssh_username = "root"
|
|
405
|
+
add_key_request = AddSSHKeyRequest(
|
|
406
|
+
public_key=public_key,
|
|
407
|
+
username=ssh_username,
|
|
408
|
+
)
|
|
422
409
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
410
|
+
with get_http_client() as client:
|
|
411
|
+
add_key_response = sessions_add_ssh_key.sync(
|
|
412
|
+
client=client,
|
|
413
|
+
session_id=session_id,
|
|
414
|
+
body=add_key_request,
|
|
415
|
+
x_api_key=api_key,
|
|
428
416
|
)
|
|
429
417
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
)
|
|
418
|
+
if not json_output:
|
|
419
|
+
# Debug: show full response
|
|
420
|
+
console.print("[yellow]DEBUG add_ssh_key response:[/yellow]")
|
|
421
|
+
console.print(f" success: {add_key_response.success}")
|
|
422
|
+
|
|
423
|
+
# Show results for each job
|
|
424
|
+
for jid, result in add_key_response.results.items():
|
|
425
|
+
console.print(f" [cyan]Job {jid}:[/cyan]")
|
|
426
|
+
console.print(f" success: {result.success}")
|
|
427
|
+
console.print(f" error: {result.error}")
|
|
428
|
+
console.print(" output:")
|
|
429
|
+
if result.output:
|
|
430
|
+
console.print(result.output)
|
|
431
|
+
else:
|
|
432
|
+
console.print(" (none)")
|
|
433
|
+
if result.success:
|
|
434
|
+
console.print(f" [green]✓[/green] {jid}: SSH key added")
|
|
435
|
+
else:
|
|
436
|
+
console.print(f" [red]✗[/red] {jid}: {result.error}")
|
|
437
437
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
body=setup_request,
|
|
443
|
-
x_api_key=api_key,
|
|
444
|
-
)
|
|
438
|
+
if add_key_response.success:
|
|
439
|
+
if not json_output:
|
|
440
|
+
console.print("[green]SSH setup complete![/green]")
|
|
441
|
+
console.print(" [cyan]SSH:[/cyan] plato sandbox ssh")
|
|
445
442
|
else:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
ssh_public_key=ssh_public_key,
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
with get_http_client() as client:
|
|
452
|
-
_setup_response = setup_root_access.sync(
|
|
453
|
-
client=client,
|
|
454
|
-
public_id=job_id,
|
|
455
|
-
body=setup_root_request,
|
|
456
|
-
x_api_key=api_key,
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
if not json_output:
|
|
460
|
-
console.print("[green]SSH setup complete![/green]")
|
|
461
|
-
console.print(f" [cyan]SSH:[/cyan] ssh -F {ssh_config_path} {ssh_host}")
|
|
443
|
+
if not json_output:
|
|
444
|
+
console.print("[red]SSH key setup failed - SSH may not work[/red]")
|
|
462
445
|
|
|
463
446
|
except Exception as e:
|
|
464
447
|
if not json_output:
|
|
@@ -482,16 +465,15 @@ def sandbox_start(
|
|
|
482
465
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
483
466
|
**state_extras,
|
|
484
467
|
}
|
|
485
|
-
# Add SSH
|
|
486
|
-
if ssh_host:
|
|
487
|
-
state["ssh_host"] = ssh_host
|
|
488
|
-
if ssh_config_path:
|
|
489
|
-
state["ssh_config_path"] = ssh_config_path
|
|
468
|
+
# Add SSH private key path if available
|
|
490
469
|
if ssh_private_key_path:
|
|
491
470
|
state["ssh_private_key_path"] = ssh_private_key_path
|
|
492
471
|
# Add heartbeat PID
|
|
493
472
|
if heartbeat_pid:
|
|
494
473
|
state["heartbeat_pid"] = heartbeat_pid
|
|
474
|
+
# Add network connection status
|
|
475
|
+
if connect_network:
|
|
476
|
+
state["network_connected"] = True
|
|
495
477
|
save_sandbox_state(state, working_dir)
|
|
496
478
|
|
|
497
479
|
# Close the plato client (heartbeat process keeps session alive)
|
|
@@ -504,11 +486,9 @@ def sandbox_start(
|
|
|
504
486
|
"job_id": job_id,
|
|
505
487
|
"public_url": display_url, # Full URL with _plato_router_target
|
|
506
488
|
}
|
|
507
|
-
if
|
|
508
|
-
output["
|
|
509
|
-
|
|
510
|
-
output["ssh_config_path"] = ssh_config_path
|
|
511
|
-
output["ssh_command"] = f"ssh -F {ssh_config_path} {ssh_host}"
|
|
489
|
+
if ssh_private_key_path:
|
|
490
|
+
output["ssh_private_key_path"] = ssh_private_key_path
|
|
491
|
+
output["ssh_command"] = "plato sandbox ssh"
|
|
512
492
|
console.print(json.dumps(output))
|
|
513
493
|
else:
|
|
514
494
|
console.print("\n[green]Sandbox started successfully![/green]")
|
|
@@ -517,15 +497,36 @@ def sandbox_start(
|
|
|
517
497
|
if public_url:
|
|
518
498
|
display_url = format_public_url_with_router_target(public_url, sim_name)
|
|
519
499
|
console.print(f" [cyan]Public URL:[/cyan] {display_url}")
|
|
520
|
-
if
|
|
521
|
-
console.print(
|
|
500
|
+
if ssh_private_key_path:
|
|
501
|
+
console.print(" [cyan]SSH:[/cyan] plato sandbox ssh")
|
|
502
|
+
# Warn if using host-only routing (no VM-to-VM mesh)
|
|
503
|
+
if connect_network and hasattr(session, "network_host_only") and session.network_host_only:
|
|
504
|
+
console.print("\n[yellow]Warning: WireGuard not available in VM - using host-only routing[/yellow]")
|
|
505
|
+
console.print("[yellow] SSH from outside works, but VM-to-VM networking is disabled[/yellow]")
|
|
522
506
|
console.print(f"\n[dim]State saved to {SANDBOX_FILE}[/dim]")
|
|
523
507
|
|
|
524
508
|
except Exception as e:
|
|
525
509
|
if json_output:
|
|
526
510
|
console.print(json.dumps({"error": str(e)}))
|
|
527
511
|
else:
|
|
528
|
-
|
|
512
|
+
error_msg = str(e)
|
|
513
|
+
# Check if it's a network connection error with VM details
|
|
514
|
+
if "Network connection failed" in error_msg or "WireGuard" in error_msg:
|
|
515
|
+
console.print("[red]Failed to start sandbox - network setup failed[/red]")
|
|
516
|
+
console.print("[yellow]VM error:[/yellow]")
|
|
517
|
+
# Clean up error message - remove SSH warnings and format nicely
|
|
518
|
+
clean_lines = []
|
|
519
|
+
for line in error_msg.split("\n"):
|
|
520
|
+
line = line.strip()
|
|
521
|
+
# Skip SSH warnings
|
|
522
|
+
if line.startswith("Warning:") or "known hosts" in line:
|
|
523
|
+
continue
|
|
524
|
+
if line:
|
|
525
|
+
clean_lines.append(line)
|
|
526
|
+
for line in clean_lines:
|
|
527
|
+
console.print(f" {line}")
|
|
528
|
+
else:
|
|
529
|
+
console.print(f"[red]Failed to start sandbox: {e}[/red]")
|
|
529
530
|
raise typer.Exit(1) from e
|
|
530
531
|
|
|
531
532
|
|
|
@@ -542,30 +543,23 @@ def sandbox_snapshot(
|
|
|
542
543
|
messaging_port: int = typer.Option(None, "--messaging-port", help="Override messaging port"),
|
|
543
544
|
target: str = typer.Option(None, "--target", help="Override target domain (e.g., myapp.web.plato.so)"),
|
|
544
545
|
):
|
|
545
|
-
"""
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
'plato pm submit base'
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
plato sandbox snapshot -c # Force include local config files
|
|
563
|
-
plato sandbox snapshot --app-port 8080 # Override app port
|
|
564
|
-
|
|
565
|
-
NEXT STEPS:
|
|
566
|
-
|
|
567
|
-
After snapshot, you can submit for review:
|
|
568
|
-
plato pm submit base # Reads artifact_id from .sandbox.yaml
|
|
546
|
+
"""Create a snapshot of the current sandbox state.
|
|
547
|
+
|
|
548
|
+
Captures the current VM state as an artifact that can be submitted for review or
|
|
549
|
+
used as a starting point for future sandboxes. The artifact ID is saved to
|
|
550
|
+
.sandbox.yaml so it can be used by 'plato pm submit base'.
|
|
551
|
+
|
|
552
|
+
For sandboxes started from config (-c), automatically includes plato-config.yml and
|
|
553
|
+
flows.yml in the snapshot. For sandboxes started from an artifact, config is inherited
|
|
554
|
+
from the parent.
|
|
555
|
+
|
|
556
|
+
Options:
|
|
557
|
+
-j, --json: Output results as JSON instead of formatted text
|
|
558
|
+
-c, --include-config: Force including local plato-config.yml and flows.yml in the
|
|
559
|
+
snapshot. Auto-enabled for sandboxes started from config.
|
|
560
|
+
--app-port: Override the internal application port stored in the artifact
|
|
561
|
+
--messaging-port: Override the Plato messaging port stored in the artifact
|
|
562
|
+
--target: Override the target domain (e.g., myapp.web.plato.so)
|
|
569
563
|
"""
|
|
570
564
|
api_key = require_api_key()
|
|
571
565
|
state = require_sandbox_state()
|
|
@@ -660,26 +654,11 @@ def sandbox_snapshot(
|
|
|
660
654
|
|
|
661
655
|
@sandbox_app.command(name="stop")
|
|
662
656
|
def sandbox_stop():
|
|
663
|
-
"""
|
|
664
|
-
Stop and destroy the current sandbox.
|
|
657
|
+
"""Stop and destroy the current sandbox.
|
|
665
658
|
|
|
666
|
-
|
|
659
|
+
Terminates the remote VM session, stops the heartbeat background process,
|
|
660
|
+
cleans up local SSH keys created for this sandbox, and removes .sandbox.yaml.
|
|
667
661
|
Run this when you're done with the sandbox or want to start fresh.
|
|
668
|
-
|
|
669
|
-
REQUIRES:
|
|
670
|
-
|
|
671
|
-
.sandbox.yaml in current directory (created by 'plato sandbox start')
|
|
672
|
-
|
|
673
|
-
USAGE:
|
|
674
|
-
|
|
675
|
-
plato sandbox stop
|
|
676
|
-
|
|
677
|
-
WHAT IT DOES:
|
|
678
|
-
|
|
679
|
-
1. Stops the heartbeat process
|
|
680
|
-
2. Closes the remote session
|
|
681
|
-
3. Removes SSH config and keys
|
|
682
|
-
4. Deletes .sandbox.yaml
|
|
683
662
|
"""
|
|
684
663
|
api_key = require_api_key()
|
|
685
664
|
state = require_sandbox_state()
|
|
@@ -703,16 +682,9 @@ def sandbox_stop():
|
|
|
703
682
|
x_api_key=api_key,
|
|
704
683
|
)
|
|
705
684
|
|
|
706
|
-
# Clean up SSH
|
|
707
|
-
ssh_config_path = state.get("ssh_config_path")
|
|
685
|
+
# Clean up SSH key files
|
|
708
686
|
ssh_private_key_path = state.get("ssh_private_key_path")
|
|
709
687
|
|
|
710
|
-
if ssh_config_path:
|
|
711
|
-
config_file = Path(ssh_config_path)
|
|
712
|
-
if config_file.exists():
|
|
713
|
-
config_file.unlink()
|
|
714
|
-
console.print(f"[dim]Removed {ssh_config_path}[/dim]")
|
|
715
|
-
|
|
716
688
|
if ssh_private_key_path:
|
|
717
689
|
private_key_file = Path(ssh_private_key_path)
|
|
718
690
|
public_key_file = Path(ssh_private_key_path + ".pub")
|
|
@@ -731,32 +703,78 @@ def sandbox_stop():
|
|
|
731
703
|
raise typer.Exit(1) from e
|
|
732
704
|
|
|
733
705
|
|
|
734
|
-
@sandbox_app.command(name="
|
|
735
|
-
def
|
|
706
|
+
@sandbox_app.command(name="connect-network")
|
|
707
|
+
def sandbox_connect_network(
|
|
708
|
+
session_id: str = typer.Option(None, "--session", "-s", help="Session ID (uses .sandbox.yaml if not provided)"),
|
|
736
709
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
737
710
|
):
|
|
711
|
+
"""Connect all jobs in a session to a WireGuard network.
|
|
712
|
+
|
|
713
|
+
Establishes encrypted peer-to-peer networking between VMs in the session,
|
|
714
|
+
allowing SSH access from outside and VM-to-VM communication. Pre-generates
|
|
715
|
+
WireGuard keys, allocates IPs from the session network subnet, and configures
|
|
716
|
+
full mesh networking.
|
|
717
|
+
|
|
718
|
+
Options:
|
|
719
|
+
-s, --session: Session ID to connect. If not provided, reads from .sandbox.yaml
|
|
720
|
+
-j, --json: Output results as JSON instead of formatted text
|
|
738
721
|
"""
|
|
739
|
-
|
|
722
|
+
api_key = require_api_key()
|
|
723
|
+
|
|
724
|
+
# Get session ID from argument or .sandbox.yaml
|
|
725
|
+
if session_id is None:
|
|
726
|
+
state = require_sandbox_state()
|
|
727
|
+
session_id = require_sandbox_field(state, "session_id")
|
|
728
|
+
|
|
729
|
+
console.print(f"[cyan]Connecting session {session_id} to network...[/cyan]")
|
|
730
|
+
|
|
731
|
+
try:
|
|
732
|
+
with get_http_client() as client:
|
|
733
|
+
result = sessions_connect_network.sync(
|
|
734
|
+
client=client,
|
|
735
|
+
session_id=session_id,
|
|
736
|
+
body=ConnectNetworkRequest(),
|
|
737
|
+
x_api_key=api_key,
|
|
738
|
+
)
|
|
740
739
|
|
|
741
|
-
|
|
742
|
-
|
|
740
|
+
if json_output:
|
|
741
|
+
console.print_json(data=result)
|
|
742
|
+
else:
|
|
743
|
+
# Display results
|
|
744
|
+
statuses = result.get("statuses", {})
|
|
745
|
+
success_count = sum(1 for s in statuses.values() if s.get("success"))
|
|
746
|
+
total_count = len(statuses)
|
|
743
747
|
|
|
744
|
-
|
|
748
|
+
if success_count == total_count:
|
|
749
|
+
console.print(f"[green]All {total_count} jobs connected to network[/green]")
|
|
750
|
+
else:
|
|
751
|
+
console.print(f"[yellow]{success_count}/{total_count} jobs connected[/yellow]")
|
|
752
|
+
|
|
753
|
+
for job_id, status in statuses.items():
|
|
754
|
+
if status.get("success"):
|
|
755
|
+
wg_ip = status.get("wireguard_ip", "unknown")
|
|
756
|
+
console.print(f" [green]✓[/green] {job_id}: {wg_ip}")
|
|
757
|
+
else:
|
|
758
|
+
error = status.get("error", "unknown error")
|
|
759
|
+
console.print(f" [red]✗[/red] {job_id}: {error}")
|
|
745
760
|
|
|
746
|
-
|
|
761
|
+
except Exception as e:
|
|
762
|
+
console.print(f"[red]Failed to connect network: {e}[/red]")
|
|
763
|
+
raise typer.Exit(1) from e
|
|
747
764
|
|
|
748
|
-
USAGE:
|
|
749
765
|
|
|
750
|
-
|
|
751
|
-
|
|
766
|
+
@sandbox_app.command(name="status")
|
|
767
|
+
def sandbox_status(
|
|
768
|
+
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
769
|
+
):
|
|
770
|
+
"""Show current sandbox status and connection info.
|
|
752
771
|
|
|
753
|
-
|
|
772
|
+
Displays information from .sandbox.yaml combined with live status from the API.
|
|
773
|
+
Shows session ID, job ID, VM status (running/stopped/etc.), public URL for browser
|
|
774
|
+
access, SSH connection details, network connection status, and heartbeat status.
|
|
754
775
|
|
|
755
|
-
|
|
756
|
-
-
|
|
757
|
-
- VM status (running/stopped/etc.)
|
|
758
|
-
- Session ID, Job ID
|
|
759
|
-
- Service name, dataset
|
|
776
|
+
Options:
|
|
777
|
+
-j, --json: Output all status info as JSON instead of formatted text
|
|
760
778
|
"""
|
|
761
779
|
state = require_sandbox_state()
|
|
762
780
|
|
|
@@ -876,10 +894,14 @@ def sandbox_status(
|
|
|
876
894
|
console.print(f" [cyan]Created:[/cyan] {state.get('created_at')}")
|
|
877
895
|
|
|
878
896
|
# Display SSH command if available
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
if
|
|
882
|
-
console.print(
|
|
897
|
+
ssh_private_key_path = state.get("ssh_private_key_path")
|
|
898
|
+
job_id = state.get("job_id")
|
|
899
|
+
if ssh_private_key_path and job_id:
|
|
900
|
+
console.print(" [cyan]SSH:[/cyan] plato sandbox ssh")
|
|
901
|
+
|
|
902
|
+
# Display network connection status
|
|
903
|
+
if state.get("network_connected"):
|
|
904
|
+
console.print(" [cyan]Network:[/cyan] [green]connected[/green] (WireGuard)")
|
|
883
905
|
|
|
884
906
|
# Display heartbeat process status
|
|
885
907
|
heartbeat_pid = state.get("heartbeat_pid")
|
|
@@ -930,38 +952,24 @@ def sandbox_start_worker(
|
|
|
930
952
|
wait_timeout: int = typer.Option(240, "--wait-timeout", help="Timeout in seconds for --wait (default: 240)"),
|
|
931
953
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
932
954
|
):
|
|
933
|
-
"""
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
WORKFLOW POSITION:
|
|
952
|
-
|
|
953
|
-
1. plato sandbox start -c
|
|
954
|
-
2. plato sandbox start-services
|
|
955
|
-
3. plato sandbox flow ← verify login works first!
|
|
956
|
-
4. plato sandbox start-worker --wait ← you are here (wait ~2-3 min)
|
|
957
|
-
5. plato sandbox flow ← run login again to verify with worker
|
|
958
|
-
6. plato sandbox state --verify-no-mutations ← verify no mutations
|
|
959
|
-
7. plato sandbox snapshot
|
|
960
|
-
|
|
961
|
-
WARNING:
|
|
962
|
-
|
|
963
|
-
Starting the worker with broken login causes infinite error loops.
|
|
964
|
-
Always verify login works before starting the worker.
|
|
955
|
+
"""Start the Plato worker in the sandbox.
|
|
956
|
+
|
|
957
|
+
The worker is the Plato component that handles flow execution, database audit
|
|
958
|
+
tracking, and state management. It should be started AFTER verifying the login
|
|
959
|
+
flow works manually, since a broken login with an active worker causes error loops.
|
|
960
|
+
|
|
961
|
+
Reads the dataset configuration from plato-config.yml to configure the worker
|
|
962
|
+
with the correct services, listeners, and compute settings.
|
|
963
|
+
|
|
964
|
+
Options:
|
|
965
|
+
-s, --service: Service name to configure the worker for. Defaults to value in
|
|
966
|
+
.sandbox.yaml if not provided.
|
|
967
|
+
-d, --dataset: Dataset name from plato-config.yml (default: "base")
|
|
968
|
+
--config-path: Path to plato-config.yml. Defaults to current directory.
|
|
969
|
+
-w, --wait: After starting, poll the state API until the worker is ready.
|
|
970
|
+
Useful in scripts to ensure the worker is fully initialized.
|
|
971
|
+
--wait-timeout: Timeout in seconds for --wait (default: 240 seconds)
|
|
972
|
+
-j, --json: Output results as JSON instead of formatted text
|
|
965
973
|
"""
|
|
966
974
|
api_key = require_api_key()
|
|
967
975
|
state = require_sandbox_state()
|
|
@@ -1122,36 +1130,20 @@ def sandbox_sync(
|
|
|
1122
1130
|
timeout: int = typer.Option(120, "--timeout", "-t", help="Command timeout in seconds"),
|
|
1123
1131
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
1124
1132
|
):
|
|
1125
|
-
"""
|
|
1126
|
-
Sync local files to the sandbox VM.
|
|
1127
|
-
|
|
1128
|
-
Uploads local files to the remote sandbox. Useful for updating
|
|
1129
|
-
docker-compose.yml, flows.yml, or other config without restarting.
|
|
1130
|
-
|
|
1131
|
-
REQUIRES:
|
|
1132
|
-
|
|
1133
|
-
.sandbox.yaml in current directory (created by 'plato sandbox start')
|
|
1134
|
-
|
|
1135
|
-
USAGE:
|
|
1136
|
-
|
|
1137
|
-
plato sandbox sync # Sync current directory
|
|
1138
|
-
plato sandbox sync ./base # Sync specific directory
|
|
1139
|
-
plato sandbox sync -r /custom/path # Custom remote path
|
|
1140
|
-
|
|
1141
|
-
DEFAULT REMOTE PATH:
|
|
1142
|
-
|
|
1143
|
-
/home/plato/worktree/<service>/
|
|
1144
|
-
|
|
1145
|
-
WHAT IT SYNCS:
|
|
1133
|
+
"""Sync local files to the sandbox VM.
|
|
1146
1134
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1135
|
+
Creates a tar archive of local files and uploads it to the remote VM via the
|
|
1136
|
+
execute API. Excludes common build artifacts (.git, __pycache__, node_modules,
|
|
1137
|
+
.venv, etc.) to reduce transfer size.
|
|
1150
1138
|
|
|
1151
|
-
|
|
1139
|
+
Arguments:
|
|
1140
|
+
path: Local path to sync (default: current directory)
|
|
1152
1141
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1142
|
+
Options:
|
|
1143
|
+
-r, --remote-path: Destination path on the VM. Defaults to
|
|
1144
|
+
/home/plato/worktree/<service> based on the service in .sandbox.yaml
|
|
1145
|
+
-t, --timeout: Command timeout in seconds for the extract operation (default: 120)
|
|
1146
|
+
-j, --json: Output results as JSON instead of formatted text
|
|
1155
1147
|
"""
|
|
1156
1148
|
api_key = require_api_key()
|
|
1157
1149
|
state = require_sandbox_state()
|
|
@@ -1367,41 +1359,25 @@ def sandbox_flow(
|
|
|
1367
1359
|
local: bool = typer.Option(False, "--local", "-l", help="Force using local flows.yml only"),
|
|
1368
1360
|
api: bool = typer.Option(False, "--api", "-a", help="Force fetching flows from API only"),
|
|
1369
1361
|
):
|
|
1370
|
-
"""
|
|
1371
|
-
Execute a test flow against the running sandbox.
|
|
1372
|
-
|
|
1373
|
-
Runs a flow (like login) to verify it works before starting the worker.
|
|
1374
|
-
Opens a browser and executes the flow steps automatically.
|
|
1375
|
-
|
|
1376
|
-
REQUIRES:
|
|
1377
|
-
|
|
1378
|
-
.sandbox.yaml in current directory (created by 'plato sandbox start')
|
|
1379
|
-
Either:
|
|
1380
|
-
- Local plato-config.yml with flows_path pointing to flows.yml
|
|
1381
|
-
- Or sandbox started from artifact (flows fetched from API)
|
|
1382
|
-
|
|
1383
|
-
USAGE:
|
|
1362
|
+
"""Execute a test flow against the running sandbox.
|
|
1384
1363
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
plato sandbox flow --api # Force API flows (from artifact)
|
|
1364
|
+
Runs a named flow (like "login") using Playwright to verify it works correctly.
|
|
1365
|
+
Opens a visible browser window, navigates to the sandbox public URL, and executes
|
|
1366
|
+
the flow steps automatically. Useful for testing login flows before starting
|
|
1367
|
+
the worker.
|
|
1390
1368
|
|
|
1391
|
-
|
|
1369
|
+
By default, looks for flows in local flows.yml (path from plato-config.yml
|
|
1370
|
+
metadata.flows_path), then falls back to fetching from the API if the sandbox
|
|
1371
|
+
was started from an artifact.
|
|
1392
1372
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1373
|
+
Options:
|
|
1374
|
+
-f, --flow-name: Name of the flow to execute from flows.yml (default: "login")
|
|
1375
|
+
-l, --local: Only use local flows.yml file. Errors if not found instead of
|
|
1376
|
+
falling back to API.
|
|
1377
|
+
-a, --api: Only fetch flows from the API (from the artifact). Ignores any
|
|
1378
|
+
local flows.yml file.
|
|
1398
1379
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
1. Local flows.yml (from plato-config.yml metadata.flows_path)
|
|
1402
|
-
2. API (fetched from artifact if started from simulator)
|
|
1403
|
-
|
|
1404
|
-
Use --local or --api to override this behavior.
|
|
1380
|
+
Note: --local and --api are mutually exclusive.
|
|
1405
1381
|
"""
|
|
1406
1382
|
# Validate mutually exclusive flags
|
|
1407
1383
|
if local and api:
|
|
@@ -1581,30 +1557,19 @@ def sandbox_state_cmd(
|
|
|
1581
1557
|
),
|
|
1582
1558
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
1583
1559
|
):
|
|
1584
|
-
"""
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
Useful
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
plato sandbox state # Show current state
|
|
1599
|
-
plato sandbox state --verify-no-mutations # Exit 1 if mutations found
|
|
1600
|
-
plato sandbox state -v # Short form
|
|
1601
|
-
|
|
1602
|
-
USED DURING REVIEW:
|
|
1603
|
-
|
|
1604
|
-
1. Run login flow
|
|
1605
|
-
2. plato sandbox state -v ← should pass (no mutations)
|
|
1606
|
-
3. Make a change in the app
|
|
1607
|
-
4. plato sandbox state ← should show mutations
|
|
1560
|
+
"""Get the database state/mutations from the simulator.
|
|
1561
|
+
|
|
1562
|
+
Queries the worker to show what database changes have been detected since the last
|
|
1563
|
+
reset. Displays mutations grouped by table and operation type (INSERT/UPDATE/DELETE).
|
|
1564
|
+
Useful for verifying that login flows don't cause unwanted database mutations and
|
|
1565
|
+
that the audit system is properly tracking changes.
|
|
1566
|
+
|
|
1567
|
+
Options:
|
|
1568
|
+
-v, --verify-no-mutations: Exit with code 1 if any mutations are detected.
|
|
1569
|
+
Useful for CI/automation to verify login doesn't cause database changes.
|
|
1570
|
+
If mutations are found, the exit code indicates failure.
|
|
1571
|
+
-j, --json: Output the full state response as JSON instead of formatted text.
|
|
1572
|
+
Includes has_mutations and has_error fields for scripting.
|
|
1608
1573
|
"""
|
|
1609
1574
|
sandbox_state = require_sandbox_state()
|
|
1610
1575
|
api_key = require_api_key()
|
|
@@ -1790,33 +1755,19 @@ def sandbox_clear_audit(
|
|
|
1790
1755
|
dataset: str = typer.Option("base", "--dataset", "-d", help="Dataset name"),
|
|
1791
1756
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
1792
1757
|
):
|
|
1793
|
-
"""
|
|
1794
|
-
Clear the audit_log table(s) in the sandbox database.
|
|
1795
|
-
|
|
1796
|
-
Truncates all audit_log tables to reset mutation tracking. Use this after
|
|
1797
|
-
initial setup/login to clear any mutations before running a clean login flow.
|
|
1798
|
-
|
|
1799
|
-
REQUIRES:
|
|
1758
|
+
"""Clear the audit_log table(s) in the sandbox database.
|
|
1800
1759
|
|
|
1801
|
-
|
|
1802
|
-
|
|
1760
|
+
Truncates all audit_log tables to reset mutation tracking. Use this after initial
|
|
1761
|
+
setup or login has generated expected mutations, so you can verify that subsequent
|
|
1762
|
+
login flows don't create new mutations.
|
|
1803
1763
|
|
|
1804
|
-
|
|
1764
|
+
Reads database connection info from plato-config.yml listeners and executes the
|
|
1765
|
+
appropriate SQL (PostgreSQL TRUNCATE or MySQL DELETE) via SSH to the sandbox VM.
|
|
1805
1766
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
WORKFLOW POSITION:
|
|
1811
|
-
|
|
1812
|
-
1. plato sandbox start -c
|
|
1813
|
-
2. plato sandbox start-services
|
|
1814
|
-
3. plato sandbox start-worker --wait
|
|
1815
|
-
4. (agent does initial login/setup, generating mutations)
|
|
1816
|
-
5. plato sandbox clear-audit ← you are here
|
|
1817
|
-
6. plato sandbox flow ← clean login flow
|
|
1818
|
-
7. plato sandbox state --verify-no-mutations ← should pass now
|
|
1819
|
-
8. plato sandbox snapshot
|
|
1767
|
+
Options:
|
|
1768
|
+
--config-path: Path to plato-config.yml file (default: looks in current directory)
|
|
1769
|
+
-d, --dataset: Dataset name to read listener configuration from (default: "base")
|
|
1770
|
+
-j, --json: Output results as JSON instead of formatted text
|
|
1820
1771
|
"""
|
|
1821
1772
|
state = require_sandbox_state()
|
|
1822
1773
|
|
|
@@ -1932,26 +1883,15 @@ def sandbox_clear_audit(
|
|
|
1932
1883
|
|
|
1933
1884
|
@sandbox_app.command(name="audit-ui")
|
|
1934
1885
|
def sandbox_audit_ui():
|
|
1935
|
-
"""
|
|
1936
|
-
Launch Streamlit UI for configuring database audit rules.
|
|
1937
|
-
|
|
1938
|
-
Opens a visual interface to help configure audit_ignore_tables
|
|
1939
|
-
in plato-config.yml. Useful when you see unwanted mutations
|
|
1940
|
-
during review (like session tables, timestamps, etc.).
|
|
1941
|
-
|
|
1942
|
-
REQUIRES:
|
|
1886
|
+
"""Launch Streamlit UI for configuring database audit rules.
|
|
1943
1887
|
|
|
1944
|
-
|
|
1888
|
+
Opens a visual web interface to help configure audit_ignore_tables in plato-config.yml.
|
|
1889
|
+
The UI shows database tables and their recent mutations, making it easy to identify
|
|
1890
|
+
which tables or columns should be ignored (like session tables, last_login timestamps,
|
|
1891
|
+
etc. that change on every login).
|
|
1945
1892
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
plato sandbox audit-ui
|
|
1949
|
-
|
|
1950
|
-
WHEN TO USE:
|
|
1951
|
-
|
|
1952
|
-
- Review shows mutations after login (sessions, timestamps)
|
|
1953
|
-
- Need to figure out which tables/columns to ignore
|
|
1954
|
-
- Want visual help building audit_ignore_tables config
|
|
1893
|
+
Requires streamlit and database drivers to be installed:
|
|
1894
|
+
pip install streamlit psycopg2-binary pymysql
|
|
1955
1895
|
"""
|
|
1956
1896
|
# Check if streamlit is installed
|
|
1957
1897
|
if not shutil.which("streamlit"):
|
|
@@ -2117,40 +2057,20 @@ def _stop_heartbeat_process(pid: int) -> bool:
|
|
|
2117
2057
|
def sandbox_start_services(
|
|
2118
2058
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
2119
2059
|
):
|
|
2120
|
-
"""
|
|
2121
|
-
Deploy and start docker compose services on the sandbox.
|
|
2122
|
-
|
|
2123
|
-
Syncs your local code to the VM and starts the containers defined
|
|
2124
|
-
in plato-config.yml. This is the main command for deploying your app.
|
|
2125
|
-
|
|
2126
|
-
REQUIRES:
|
|
2127
|
-
|
|
2128
|
-
.sandbox.yaml in current directory (created by 'plato sandbox start -c')
|
|
2129
|
-
plato-config.yml with services defined
|
|
2130
|
-
|
|
2131
|
-
USAGE:
|
|
2132
|
-
|
|
2133
|
-
plato sandbox start-services # Deploy and start containers
|
|
2134
|
-
plato sandbox start-services --json # JSON output
|
|
2135
|
-
|
|
2136
|
-
WHAT IT DOES:
|
|
2137
|
-
|
|
2138
|
-
1. Pushes local code to Plato Hub (Gitea)
|
|
2139
|
-
2. Clones code on VM via SSH
|
|
2140
|
-
3. Runs 'docker compose up -d' on VM
|
|
2141
|
-
4. Waits for containers to be healthy
|
|
2142
|
-
|
|
2143
|
-
WORKFLOW POSITION:
|
|
2060
|
+
"""Deploy and start docker compose services on the sandbox.
|
|
2144
2061
|
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2062
|
+
Syncs your local code to the sandbox VM and starts containers. The process:
|
|
2063
|
+
1. Gets Gitea credentials and pushes local code to a new branch on Plato Hub
|
|
2064
|
+
2. Clones the code on the VM via SSH
|
|
2065
|
+
3. Runs 'docker compose up -d' for each docker-compose service defined in
|
|
2066
|
+
the plato-config.yml services section
|
|
2150
2067
|
|
|
2151
|
-
|
|
2068
|
+
Run this command again after making local changes to re-sync and restart containers.
|
|
2069
|
+
Requires SSH to be configured (network is enabled by default).
|
|
2152
2070
|
|
|
2153
|
-
|
|
2071
|
+
Options:
|
|
2072
|
+
-j, --json: Output results as JSON instead of formatted text. Includes
|
|
2073
|
+
branch name, repo URL, VM path, and list of services started.
|
|
2154
2074
|
"""
|
|
2155
2075
|
api_key = require_api_key()
|
|
2156
2076
|
state = require_sandbox_state()
|