plato-sdk-v2 2.0.50__py3-none-any.whl → 2.2.4__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.
Files changed (158) hide show
  1. plato/__init__.py +7 -6
  2. plato/_generated/__init__.py +1 -1
  3. plato/_generated/api/v1/env/evaluate_session.py +3 -3
  4. plato/_generated/api/v1/env/log_state_mutation.py +4 -4
  5. plato/_generated/api/v1/sandbox/checkpoint_vm.py +3 -3
  6. plato/_generated/api/v1/sandbox/save_vm_snapshot.py +3 -3
  7. plato/_generated/api/v1/sandbox/setup_sandbox.py +8 -8
  8. plato/_generated/api/v1/session/__init__.py +2 -0
  9. plato/_generated/api/v1/session/get_sessions_for_archival.py +100 -0
  10. plato/_generated/api/v1/testcases/__init__.py +6 -2
  11. plato/_generated/api/v1/testcases/get_mutation_groups_for_testcase.py +98 -0
  12. plato/_generated/api/v1/testcases/{get_next_output_testcase_for_scoring.py → get_next_testcase_for_scoring.py} +23 -10
  13. plato/_generated/api/v1/testcases/get_testcase_metadata_for_scoring.py +74 -0
  14. plato/_generated/api/v2/__init__.py +2 -1
  15. plato/_generated/api/v2/jobs/__init__.py +4 -0
  16. plato/_generated/api/v2/jobs/checkpoint.py +3 -3
  17. plato/_generated/api/v2/jobs/disk_snapshot.py +3 -3
  18. plato/_generated/api/v2/jobs/log_for_job.py +4 -39
  19. plato/_generated/api/v2/jobs/make.py +4 -4
  20. plato/_generated/api/v2/jobs/setup_sandbox.py +97 -0
  21. plato/_generated/api/v2/jobs/snapshot.py +3 -3
  22. plato/_generated/api/v2/jobs/snapshot_store.py +91 -0
  23. plato/_generated/api/v2/sessions/__init__.py +4 -0
  24. plato/_generated/api/v2/sessions/checkpoint.py +3 -3
  25. plato/_generated/api/v2/sessions/disk_snapshot.py +3 -3
  26. plato/_generated/api/v2/sessions/evaluate.py +3 -3
  27. plato/_generated/api/v2/sessions/log_job_mutation.py +4 -39
  28. plato/_generated/api/v2/sessions/make.py +4 -4
  29. plato/_generated/api/v2/sessions/setup_sandbox.py +98 -0
  30. plato/_generated/api/v2/sessions/snapshot.py +3 -3
  31. plato/_generated/api/v2/sessions/snapshot_store.py +94 -0
  32. plato/_generated/api/v2/user/__init__.py +7 -0
  33. plato/_generated/api/v2/user/get_current_user.py +76 -0
  34. plato/_generated/models/__init__.py +174 -23
  35. plato/_sims_generator/__init__.py +19 -4
  36. plato/_sims_generator/instruction.py +203 -0
  37. plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
  38. plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
  39. plato/agents/__init__.py +107 -517
  40. plato/agents/base.py +145 -0
  41. plato/agents/build.py +61 -0
  42. plato/agents/config.py +160 -0
  43. plato/agents/logging.py +401 -0
  44. plato/agents/runner.py +161 -0
  45. plato/agents/trajectory.py +266 -0
  46. plato/chronos/__init__.py +37 -0
  47. plato/chronos/api/__init__.py +3 -0
  48. plato/chronos/api/agents/__init__.py +13 -0
  49. plato/chronos/api/agents/create_agent.py +63 -0
  50. plato/chronos/api/agents/delete_agent.py +61 -0
  51. plato/chronos/api/agents/get_agent.py +62 -0
  52. plato/chronos/api/agents/get_agent_schema.py +72 -0
  53. plato/chronos/api/agents/get_agent_versions.py +62 -0
  54. plato/chronos/api/agents/list_agents.py +57 -0
  55. plato/chronos/api/agents/lookup_agent.py +74 -0
  56. plato/chronos/api/auth/__init__.py +9 -0
  57. plato/chronos/api/auth/debug_auth_api_auth_debug_get.py +43 -0
  58. plato/chronos/api/auth/get_auth_status_api_auth_status_get.py +61 -0
  59. plato/chronos/api/auth/get_current_user_route_api_auth_me_get.py +60 -0
  60. plato/chronos/api/callback/__init__.py +11 -0
  61. plato/chronos/api/callback/push_agent_logs.py +61 -0
  62. plato/chronos/api/callback/update_agent_status.py +57 -0
  63. plato/chronos/api/callback/upload_artifacts.py +59 -0
  64. plato/chronos/api/callback/upload_logs_zip.py +57 -0
  65. plato/chronos/api/callback/upload_trajectory.py +57 -0
  66. plato/chronos/api/default/__init__.py +7 -0
  67. plato/chronos/api/default/health.py +43 -0
  68. plato/chronos/api/jobs/__init__.py +7 -0
  69. plato/chronos/api/jobs/launch_job.py +63 -0
  70. plato/chronos/api/registry/__init__.py +19 -0
  71. plato/chronos/api/registry/get_agent_schema_api_registry_agents__agent_name__schema_get.py +62 -0
  72. plato/chronos/api/registry/get_agent_versions_api_registry_agents__agent_name__versions_get.py +52 -0
  73. plato/chronos/api/registry/get_world_schema_api_registry_worlds__package_name__schema_get.py +68 -0
  74. plato/chronos/api/registry/get_world_versions_api_registry_worlds__package_name__versions_get.py +52 -0
  75. plato/chronos/api/registry/list_registry_agents_api_registry_agents_get.py +44 -0
  76. plato/chronos/api/registry/list_registry_worlds_api_registry_worlds_get.py +44 -0
  77. plato/chronos/api/runtimes/__init__.py +11 -0
  78. plato/chronos/api/runtimes/create_runtime.py +63 -0
  79. plato/chronos/api/runtimes/delete_runtime.py +61 -0
  80. plato/chronos/api/runtimes/get_runtime.py +62 -0
  81. plato/chronos/api/runtimes/list_runtimes.py +57 -0
  82. plato/chronos/api/runtimes/test_runtime.py +67 -0
  83. plato/chronos/api/secrets/__init__.py +11 -0
  84. plato/chronos/api/secrets/create_secret.py +63 -0
  85. plato/chronos/api/secrets/delete_secret.py +61 -0
  86. plato/chronos/api/secrets/get_secret.py +62 -0
  87. plato/chronos/api/secrets/list_secrets.py +57 -0
  88. plato/chronos/api/secrets/update_secret.py +68 -0
  89. plato/chronos/api/sessions/__init__.py +10 -0
  90. plato/chronos/api/sessions/get_session.py +62 -0
  91. plato/chronos/api/sessions/get_session_logs.py +72 -0
  92. plato/chronos/api/sessions/get_session_logs_download.py +62 -0
  93. plato/chronos/api/sessions/list_sessions.py +57 -0
  94. plato/chronos/api/status/__init__.py +8 -0
  95. plato/chronos/api/status/get_status_api_status_get.py +44 -0
  96. plato/chronos/api/status/get_version_info_api_version_get.py +44 -0
  97. plato/chronos/api/templates/__init__.py +11 -0
  98. plato/chronos/api/templates/create_template.py +63 -0
  99. plato/chronos/api/templates/delete_template.py +61 -0
  100. plato/chronos/api/templates/get_template.py +62 -0
  101. plato/chronos/api/templates/list_templates.py +57 -0
  102. plato/chronos/api/templates/update_template.py +68 -0
  103. plato/chronos/api/trajectories/__init__.py +8 -0
  104. plato/chronos/api/trajectories/get_trajectory.py +62 -0
  105. plato/chronos/api/trajectories/list_trajectories.py +62 -0
  106. plato/chronos/api/worlds/__init__.py +10 -0
  107. plato/chronos/api/worlds/create_world.py +63 -0
  108. plato/chronos/api/worlds/delete_world.py +61 -0
  109. plato/chronos/api/worlds/get_world.py +62 -0
  110. plato/chronos/api/worlds/list_worlds.py +57 -0
  111. plato/chronos/client.py +171 -0
  112. plato/chronos/errors.py +141 -0
  113. plato/chronos/models/__init__.py +647 -0
  114. plato/chronos/py.typed +0 -0
  115. plato/sims/cli.py +299 -123
  116. plato/sims/registry.py +77 -4
  117. plato/v1/cli/agent.py +88 -84
  118. plato/v1/cli/main.py +2 -0
  119. plato/v1/cli/pm.py +441 -119
  120. plato/v1/cli/sandbox.py +747 -191
  121. plato/v1/cli/sim.py +11 -0
  122. plato/v1/cli/verify.py +1269 -0
  123. plato/v1/cli/world.py +3 -0
  124. plato/v1/flow_executor.py +21 -17
  125. plato/v1/models/env.py +11 -11
  126. plato/v1/sdk.py +2 -2
  127. plato/v1/sync_env.py +11 -11
  128. plato/v1/sync_flow_executor.py +21 -17
  129. plato/v1/sync_sdk.py +4 -2
  130. plato/v2/__init__.py +2 -0
  131. plato/v2/async_/environment.py +20 -1
  132. plato/v2/async_/session.py +54 -3
  133. plato/v2/sync/environment.py +2 -1
  134. plato/v2/sync/session.py +52 -2
  135. plato/worlds/README.md +218 -0
  136. plato/worlds/__init__.py +54 -18
  137. plato/worlds/base.py +304 -93
  138. plato/worlds/config.py +239 -73
  139. plato/worlds/runner.py +391 -80
  140. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/METADATA +1 -3
  141. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/RECORD +143 -68
  142. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/entry_points.txt +1 -0
  143. plato/_generated/api/v2/interfaces/__init__.py +0 -27
  144. plato/_generated/api/v2/interfaces/v2_interface_browser_create.py +0 -68
  145. plato/_generated/api/v2/interfaces/v2_interface_cdp_url.py +0 -65
  146. plato/_generated/api/v2/interfaces/v2_interface_click.py +0 -64
  147. plato/_generated/api/v2/interfaces/v2_interface_close.py +0 -59
  148. plato/_generated/api/v2/interfaces/v2_interface_computer_create.py +0 -68
  149. plato/_generated/api/v2/interfaces/v2_interface_cursor.py +0 -64
  150. plato/_generated/api/v2/interfaces/v2_interface_key.py +0 -68
  151. plato/_generated/api/v2/interfaces/v2_interface_screenshot.py +0 -65
  152. plato/_generated/api/v2/interfaces/v2_interface_scroll.py +0 -70
  153. plato/_generated/api/v2/interfaces/v2_interface_type.py +0 -64
  154. plato/world/__init__.py +0 -44
  155. plato/world/base.py +0 -267
  156. plato/world/config.py +0 -139
  157. plato/world/types.py +0 -47
  158. {plato_sdk_v2-2.0.50.dist-info → plato_sdk_v2-2.2.4.dist-info}/WHEEL +0 -0
plato/v1/cli/pm.py CHANGED
@@ -3,24 +3,28 @@
3
3
  import asyncio
4
4
  import json
5
5
  import os
6
+ import re
6
7
  import shutil
7
8
  import tempfile
8
9
  from pathlib import Path
9
10
 
10
11
  import httpx
11
12
  import typer
12
- from playwright.async_api import async_playwright
13
13
  from rich.table import Table
14
14
 
15
15
  from plato._generated.api.v1.env import get_simulator_by_name, get_simulators
16
+ from plato._generated.api.v1.organization import get_organization_members
16
17
  from plato._generated.api.v1.simulator import (
17
18
  add_simulator_review,
19
+ update_simulator,
18
20
  update_simulator_status,
19
21
  update_tag,
20
22
  )
21
23
  from plato._generated.api.v2.sessions import state as sessions_state
22
24
  from plato._generated.models import (
23
25
  AddReviewRequest,
26
+ AppApiV1SimulatorRoutesUpdateSimulatorRequest,
27
+ Authentication,
24
28
  Outcome,
25
29
  ReviewType,
26
30
  UpdateStatusRequest,
@@ -35,9 +39,17 @@ from plato.v1.cli.utils import (
35
39
  require_sandbox_field,
36
40
  require_sandbox_state,
37
41
  )
42
+ from plato.v1.cli.verify import pm_verify_app
38
43
  from plato.v2.async_.client import AsyncPlato
39
44
  from plato.v2.types import Env
40
45
 
46
+ # =============================================================================
47
+ # CONSTANTS
48
+ # =============================================================================
49
+
50
+ # UUID pattern for detecting artifact IDs in sim:artifact notation
51
+ UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE)
52
+
41
53
  # =============================================================================
42
54
  # APP STRUCTURE
43
55
  # =============================================================================
@@ -50,6 +62,7 @@ submit_app = typer.Typer(help="Submit simulator artifacts for review")
50
62
  pm_app.add_typer(list_app, name="list")
51
63
  pm_app.add_typer(review_app, name="review")
52
64
  pm_app.add_typer(submit_app, name="submit")
65
+ pm_app.add_typer(pm_verify_app, name="verify")
53
66
 
54
67
 
55
68
  # =============================================================================
@@ -57,6 +70,70 @@ pm_app.add_typer(submit_app, name="submit")
57
70
  # =============================================================================
58
71
 
59
72
 
73
+ def parse_simulator_artifact(
74
+ simulator: str | None,
75
+ artifact: str | None,
76
+ require_artifact: bool = False,
77
+ command_name: str = "command",
78
+ ) -> tuple[str | None, str | None]:
79
+ """
80
+ Parse simulator and artifact from CLI args, supporting colon notation.
81
+
82
+ Supports:
83
+ -s simulator # Simulator only
84
+ -s simulator -a <artifact-uuid> # Explicit artifact
85
+ -s simulator:<artifact-uuid> # Colon notation
86
+
87
+ Args:
88
+ simulator: The -s/--simulator arg value
89
+ artifact: The -a/--artifact arg value
90
+ require_artifact: If True, artifact is required
91
+ command_name: Name of command for error messages
92
+
93
+ Returns:
94
+ (simulator_name, artifact_id) tuple
95
+ """
96
+ simulator_name = None
97
+ artifact_id = artifact or ""
98
+
99
+ if simulator:
100
+ # Check for colon notation: sim:artifact
101
+ if ":" in simulator:
102
+ sim_part, colon_part = simulator.split(":", 1)
103
+ simulator_name = sim_part
104
+ if UUID_PATTERN.match(colon_part):
105
+ artifact_id = colon_part
106
+ else:
107
+ console.print(f"[red]❌ Invalid artifact UUID after colon: '{colon_part}'[/red]")
108
+ console.print()
109
+ console.print("[yellow]Usage:[/yellow]")
110
+ console.print(f" plato pm {command_name} -s <simulator> # Simulator only")
111
+ console.print(f" plato pm {command_name} -s <simulator> -a <artifact-uuid> # With artifact")
112
+ console.print(f" plato pm {command_name} -s <simulator>:<artifact-uuid> # Colon notation")
113
+ raise typer.Exit(1)
114
+ else:
115
+ simulator_name = simulator
116
+
117
+ if not simulator_name:
118
+ console.print("[red]❌ Simulator name is required[/red]")
119
+ console.print()
120
+ console.print("[yellow]Usage:[/yellow]")
121
+ console.print(f" plato pm {command_name} -s <simulator> # Simulator only")
122
+ console.print(f" plato pm {command_name} -s <simulator> -a <artifact-uuid> # With artifact")
123
+ console.print(f" plato pm {command_name} -s <simulator>:<artifact-uuid> # Colon notation")
124
+ raise typer.Exit(1)
125
+
126
+ if require_artifact and not artifact_id:
127
+ console.print("[red]❌ Artifact ID is required[/red]")
128
+ console.print()
129
+ console.print("[yellow]Usage:[/yellow]")
130
+ console.print(f" plato pm {command_name} -s <simulator> -a <artifact-uuid> # With artifact flag")
131
+ console.print(f" plato pm {command_name} -s <simulator>:<artifact-uuid> # Colon notation")
132
+ raise typer.Exit(1)
133
+
134
+ return simulator_name, artifact_id or None
135
+
136
+
60
137
  def _get_base_url() -> str:
61
138
  """Get base URL with /api suffix stripped."""
62
139
  base_url = os.getenv("PLATO_BASE_URL", "https://plato.so")
@@ -95,6 +172,21 @@ def _list_pending_reviews(review_type: str):
95
172
  x_api_key=api_key,
96
173
  )
97
174
 
175
+ # Fetch organization members to map user IDs to usernames
176
+ user_id_to_name: dict[int, str] = {}
177
+ try:
178
+ members = await get_organization_members.asyncio(
179
+ client=client,
180
+ x_api_key=api_key,
181
+ )
182
+ for member in members:
183
+ user_id = member.get("id")
184
+ username = member.get("username") or member.get("email", "")
185
+ if user_id is not None:
186
+ user_id_to_name[user_id] = username
187
+ except Exception:
188
+ pass # Continue without usernames if fetch fails
189
+
98
190
  # Filter by target status
99
191
  pending_review = []
100
192
  for sim in simulators:
@@ -110,6 +202,7 @@ def _list_pending_reviews(review_type: str):
110
202
  # Build table
111
203
  table = Table(title=f"Simulators Pending {review_type.title()} Review")
112
204
  table.add_column("Name", style="cyan", no_wrap=True)
205
+ table.add_column("Assignees", style="magenta", no_wrap=True)
113
206
  table.add_column("Notes", style="white", max_width=40)
114
207
  artifact_col_name = "base_artifact_id" if review_type == "base" else "data_artifact_id"
115
208
  table.add_column(artifact_col_name, style="green", no_wrap=True)
@@ -128,7 +221,16 @@ def _list_pending_reviews(review_type: str):
128
221
  artifact_id = config.get(artifact_key, "") if isinstance(config, dict) else ""
129
222
  artifact_id = artifact_id or "-"
130
223
 
131
- table.add_row(name, notes, artifact_id)
224
+ # Get assignees based on review type (env_assignees for base, data_assignees for data)
225
+ assignee_key = "env_assignees" if review_type == "base" else "data_assignees"
226
+ assignee_ids = config.get(assignee_key, []) if isinstance(config, dict) else []
227
+ assignee_names = []
228
+ if assignee_ids:
229
+ for uid in assignee_ids:
230
+ assignee_names.append(user_id_to_name.get(uid, str(uid)))
231
+ assignees_str = ", ".join(assignee_names) if assignee_names else "-"
232
+
233
+ table.add_row(name, assignees_str, notes, artifact_id)
132
234
 
133
235
  console.print(table)
134
236
  console.print(f"\n[cyan]Total: {len(pending_review)} simulator(s) pending {review_type} review[/cyan]")
@@ -141,10 +243,19 @@ def list_base():
141
243
  """
142
244
  List simulators pending base/environment review.
143
245
 
144
- Shows all simulators with status: env_review_requested
246
+ Shows simulators waiting for environment review (status: env_review_requested).
247
+ Use this to see what needs reviewing before running 'plato pm review base'.
248
+
249
+ USAGE:
145
250
 
146
- Example:
147
251
  plato pm list base
252
+
253
+ OUTPUT COLUMNS:
254
+
255
+ - Name: Simulator name
256
+ - Assignees: Who's assigned to review
257
+ - Notes: Any notes about the simulator
258
+ - base_artifact_id: The artifact to review
148
259
  """
149
260
  _list_pending_reviews("base")
150
261
 
@@ -154,10 +265,19 @@ def list_data():
154
265
  """
155
266
  List simulators pending data review.
156
267
 
157
- Shows all simulators with status: data_review_requested
268
+ Shows simulators waiting for data review (status: data_review_requested).
269
+ Use this to see what needs reviewing before running 'plato pm review data'.
270
+
271
+ USAGE:
158
272
 
159
- Example:
160
273
  plato pm list data
274
+
275
+ OUTPUT COLUMNS:
276
+
277
+ - Name: Simulator name
278
+ - Assignees: Who's assigned to review
279
+ - Notes: Any notes about the simulator
280
+ - data_artifact_id: The artifact to review
161
281
  """
162
282
  _list_pending_reviews("data")
163
283
 
@@ -169,40 +289,50 @@ def list_data():
169
289
 
170
290
  @review_app.command(name="base")
171
291
  def review_base(
172
- simulator: str = typer.Option(None, "--simulator", "-s", help="Simulator name"),
173
- artifact: str = typer.Option(None, "--artifact", "-a", help="Artifact ID (uses base_artifact_id if not provided)"),
292
+ simulator: str = typer.Option(
293
+ None,
294
+ "--simulator",
295
+ "-s",
296
+ help="Simulator name. Supports colon notation: -s sim:<artifact-uuid>",
297
+ ),
298
+ artifact: str = typer.Option(
299
+ None,
300
+ "--artifact",
301
+ "-a",
302
+ help="Artifact UUID to review. If not provided, uses server's base_artifact_id.",
303
+ ),
304
+ skip_review: bool = typer.Option(
305
+ False,
306
+ "--skip-review",
307
+ help="Run login flow and check state, but skip interactive review. For automated verification.",
308
+ ),
174
309
  ):
175
310
  """
176
- Review base/environment artifact session (reviewer testing the environment).
311
+ Review base/environment artifact for a simulator.
177
312
 
178
- Opens simulator with artifact in browser for manual testing.
179
- At the end, prompts to pass (→ env_approved) or reject (→ env_in_progress).
313
+ Opens the simulator in a browser for manual testing. After testing,
314
+ you can pass (→ env_approved) or reject (→ env_in_progress).
180
315
 
181
- Requires simulator status: env_review_requested
316
+ SPECIFYING SIMULATOR AND ARTIFACT:
182
317
 
183
- Example:
184
- plato pm review base --simulator espocrm
185
- plato pm review base -s espocrm -a abc123
186
- """
187
- api_key = require_api_key()
318
+ -s <simulator> Use server's base_artifact_id
319
+ -s <simulator> -a <artifact-uuid> Explicit artifact
320
+ -s <simulator>:<artifact-uuid> Colon notation (same as above)
188
321
 
189
- # Track if simulator was provided via CLI arg
190
- simulator_provided_via_arg = simulator is not None
322
+ EXAMPLES:
191
323
 
192
- # Use arg or prompt for simulator name
193
- simulator_name = simulator
194
- if not simulator_name:
195
- simulator_name = typer.prompt("Enter simulator name").strip()
196
- if not simulator_name:
197
- console.print("[red]❌ Simulator name is required[/red]")
198
- raise typer.Exit(1)
324
+ plato pm review base -s espocrm
325
+ plato pm review base -s espocrm -a e9c25ca5-1234-5678-9abc-def012345678
326
+ plato pm review base -s espocrm:e9c25ca5-1234-5678-9abc-def012345678
199
327
 
200
- # Artifact ID: use arg, or prompt only if simulator was NOT provided via arg
201
- artifact_id_input = artifact or ""
202
- if not artifact_id_input and not simulator_provided_via_arg:
203
- artifact_id_input = typer.prompt(
204
- "Enter artifact ID (or press Enter to use base_artifact_id)", default=""
205
- ).strip()
328
+ Requires simulator status: env_review_requested
329
+ """
330
+ api_key = require_api_key()
331
+
332
+ # Parse simulator and artifact from args (artifact not required - falls back to server config)
333
+ simulator_name, artifact_id_input = parse_simulator_artifact(
334
+ simulator, artifact, require_artifact=False, command_name="review base"
335
+ )
206
336
 
207
337
  async def _review_base():
208
338
  base_url = _get_base_url()
@@ -215,6 +345,9 @@ def review_base(
215
345
  try:
216
346
  http_client = plato._http
217
347
 
348
+ # simulator_name is guaranteed set by parse_simulator_artifact (or we exit)
349
+ assert simulator_name is not None, "simulator_name must be set"
350
+
218
351
  # Get simulator by name
219
352
  sim = await get_simulator_by_name.asyncio(
220
353
  client=http_client,
@@ -227,10 +360,16 @@ def review_base(
227
360
 
228
361
  console.print(f"[cyan]Current status:[/cyan] {current_status}")
229
362
 
230
- # Use provided artifact ID or fall back to base_artifact_id from config
231
- artifact_id = artifact_id_input if artifact_id_input else current_config.get("base_artifact_id")
363
+ # Use provided artifact ID or fall back to base_artifact_id from server config
364
+ artifact_id: str | None = artifact_id_input if artifact_id_input else current_config.get("base_artifact_id")
232
365
  if not artifact_id:
233
- console.print("[red]❌ No artifact ID provided and simulator has no base_artifact_id set[/red]")
366
+ console.print("[red]❌ No artifact ID provided.[/red]")
367
+ console.print(
368
+ "[yellow]This simulator hasn't been submitted yet, so there's no base artifact on record.[/yellow]"
369
+ )
370
+ console.print(
371
+ "[yellow]Specify the artifact ID from your snapshot using: plato pm review base --artifact <artifact_id>[/yellow]"
372
+ )
234
373
  raise typer.Exit(1)
235
374
 
236
375
  console.print(f"[cyan]Using artifact:[/cyan] {artifact_id}")
@@ -259,6 +398,8 @@ def review_base(
259
398
 
260
399
  # Launch Playwright browser and login
261
400
  console.print("[cyan]Launching browser and logging in...[/cyan]")
401
+ from playwright.async_api import async_playwright
402
+
262
403
  playwright = await async_playwright().start()
263
404
  browser = await playwright.chromium.launch(headless=False)
264
405
 
@@ -272,6 +413,60 @@ def review_base(
272
413
  if public_url:
273
414
  await page.goto(public_url)
274
415
 
416
+ # ALWAYS check state after login to verify no mutations
417
+ console.print("\n[cyan]Checking environment state after login...[/cyan]")
418
+ has_mutations = False
419
+ has_errors = False
420
+ try:
421
+ state_response = await sessions_state.asyncio(
422
+ client=http_client,
423
+ session_id=session.session_id,
424
+ merge_mutations=True,
425
+ x_api_key=api_key,
426
+ )
427
+ if state_response and state_response.results:
428
+ for jid, result in state_response.results.items():
429
+ state_data = result.state if hasattr(result, "state") else result
430
+ console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
431
+
432
+ if isinstance(state_data, dict):
433
+ # Check for error in state response
434
+ if "error" in state_data:
435
+ has_errors = True
436
+ console.print("\n[bold red]❌ State API Error:[/bold red]")
437
+ console.print(f"[red]{state_data['error']}[/red]")
438
+ continue
439
+
440
+ mutations = state_data.pop("mutations", [])
441
+ console.print("\n[bold]State:[/bold]")
442
+ console.print(json.dumps(state_data, indent=2, default=str))
443
+ if mutations:
444
+ has_mutations = True
445
+ console.print(f"\n[bold red]Mutations ({len(mutations)}):[/bold red]")
446
+ console.print(json.dumps(mutations, indent=2, default=str))
447
+ else:
448
+ console.print("\n[green]No mutations recorded[/green]")
449
+ else:
450
+ console.print(f"[yellow]Unexpected state format: {type(state_data)}[/yellow]")
451
+
452
+ if has_errors:
453
+ console.print("\n[bold red]❌ State check failed due to errors![/bold red]")
454
+ console.print("[yellow]The worker may not be properly connected.[/yellow]")
455
+ elif has_mutations:
456
+ console.print("\n[bold red]⚠️ WARNING: Login flow created mutations![/bold red]")
457
+ console.print("[yellow]The login flow should NOT modify database state.[/yellow]")
458
+ else:
459
+ console.print("\n[bold green]✅ Login flow verified - no mutations created[/bold green]")
460
+ else:
461
+ console.print("[yellow]No state data available[/yellow]")
462
+ except Exception as e:
463
+ console.print(f"[red]❌ Error getting state: {e}[/red]")
464
+
465
+ # If skip_review, exit without interactive loop
466
+ if skip_review:
467
+ console.print("\n[cyan]Skipping interactive review (--skip-review)[/cyan]")
468
+ return
469
+
275
470
  console.print("\n" + "=" * 60)
276
471
  console.print("[bold green]Environment Review Session Active[/bold green]")
277
472
  console.print("=" * 60)
@@ -281,7 +476,7 @@ def review_base(
281
476
  console.print("=" * 60)
282
477
 
283
478
  # Show recent env review if available
284
- reviews = current_config.get("reviews", [])
479
+ reviews = current_config.get("reviews") or []
285
480
  env_reviews = [r for r in reviews if r.get("review_type") == "env"]
286
481
  if env_reviews:
287
482
  env_reviews.sort(key=lambda r: r.get("timestamp_iso", ""), reverse=True)
@@ -320,9 +515,16 @@ def review_base(
320
515
  if state_response and state_response.results:
321
516
  for jid, result in state_response.results.items():
322
517
  state_data = result.state if hasattr(result, "state") else result
518
+ console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
519
+
323
520
  if isinstance(state_data, dict):
521
+ # Check for error in state response
522
+ if "error" in state_data:
523
+ console.print("\n[bold red]❌ State API Error:[/bold red]")
524
+ console.print(f"[red]{state_data['error']}[/red]")
525
+ continue
526
+
324
527
  mutations = state_data.pop("mutations", [])
325
- console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
326
528
  console.print("\n[bold]State:[/bold]")
327
529
  console.print(json.dumps(state_data, indent=2, default=str))
328
530
  if mutations:
@@ -331,7 +533,6 @@ def review_base(
331
533
  else:
332
534
  console.print("\n[yellow]No mutations recorded[/yellow]")
333
535
  else:
334
- console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
335
536
  console.print(json.dumps(state_data, indent=2, default=str))
336
537
  else:
337
538
  console.print("[yellow]No state data available[/yellow]")
@@ -407,9 +608,11 @@ def review_base(
407
608
  console.print(f"[cyan]Status:[/cyan] {current_status} → {new_status}")
408
609
 
409
610
  # If passed, automatically tag artifact as prod-latest
410
- if outcome == "pass":
611
+ if outcome == "pass" and artifact_id:
411
612
  console.print("\n[cyan]Tagging artifact as prod-latest...[/cyan]")
412
613
  try:
614
+ # simulator_name and artifact_id are guaranteed to be set at this point
615
+ assert simulator_name is not None
413
616
  await update_tag.asyncio(
414
617
  client=http_client,
415
618
  body=UpdateTagRequest(
@@ -461,57 +664,89 @@ def review_base(
461
664
  @review_app.command(name="data")
462
665
  def review_data(
463
666
  simulator: str = typer.Option(
464
- None, "--simulator", "-s", help="Simulator name (navigates to {simulator}.web.plato.so)"
667
+ None,
668
+ "--simulator",
669
+ "-s",
670
+ help="Simulator name. Supports colon notation: -s sim:<artifact-uuid>",
671
+ ),
672
+ artifact: str = typer.Option(
673
+ None,
674
+ "--artifact",
675
+ "-a",
676
+ help="Artifact UUID to review. If not provided, uses server's data_artifact_id.",
465
677
  ),
466
678
  ):
467
679
  """
468
680
  Launch browser with EnvGen Recorder extension for data review.
469
681
 
470
- Opens Chrome with the extension installed. If --simulator is provided,
471
- navigates to {simulator}.web.plato.so and auto-loads the sim in the extension.
472
- Otherwise navigates to sims.plato.so.
682
+ Opens Chrome with the EnvGen Recorder extension installed for reviewing
683
+ data artifacts. Close the browser when done.
684
+
685
+ SPECIFYING SIMULATOR AND ARTIFACT:
473
686
 
474
- When done, simply close the browser to exit.
687
+ -s <simulator> Use server's data_artifact_id
688
+ -s <simulator> -a <artifact-uuid> Explicit artifact
689
+ -s <simulator>:<artifact-uuid> Colon notation (same as above)
690
+
691
+ EXAMPLES:
475
692
 
476
- Example:
477
- plato pm review data
478
693
  plato pm review data -s fathom
694
+ plato pm review data -s fathom -a e9c25ca5-1234-5678-9abc-def012345678
695
+ plato pm review data -s fathom:e9c25ca5-1234-5678-9abc-def012345678
696
+
697
+ Requires simulator status: data_review_requested
479
698
  """
480
699
  api_key = require_api_key()
481
700
 
701
+ # Parse simulator and artifact from args (artifact not required - falls back to server config)
702
+ simulator_name, artifact_id = parse_simulator_artifact(
703
+ simulator, artifact, require_artifact=False, command_name="review data"
704
+ )
705
+
482
706
  # Determine target URL based on simulator
483
- if simulator:
484
- target_url = f"https://{simulator}.web.plato.so"
485
- console.print(f"[cyan]Simulator:[/cyan] {simulator}")
486
- else:
487
- target_url = "https://sims.plato.so"
707
+ target_url = f"https://{simulator_name}.web.plato.so"
708
+ console.print(f"[cyan]Simulator:[/cyan] {simulator_name}")
488
709
 
489
- # If simulator provided, fetch and show recent data reviews
710
+ # Fetch simulator config and get artifact ID if not provided
490
711
  recent_review = None
491
- if simulator:
492
712
 
493
- async def _fetch_recent_review():
494
- base_url = _get_base_url()
495
- async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
496
- try:
497
- sim = await get_simulator_by_name.asyncio(
498
- client=client,
499
- name=simulator,
500
- x_api_key=api_key,
501
- )
502
- config = sim.config or {}
503
- reviews = config.get("reviews", [])
504
- # Find most recent data review
505
- data_reviews = [r for r in reviews if r.get("review_type") == "data"]
506
- if data_reviews:
507
- # Sort by timestamp (most recent first)
508
- data_reviews.sort(key=lambda r: r.get("timestamp_iso", ""), reverse=True)
509
- return data_reviews[0]
510
- except Exception as e:
511
- console.print(f"[yellow]⚠️ Could not fetch recent reviews: {e}[/yellow]")
512
- return None
713
+ async def _fetch_artifact_info():
714
+ nonlocal artifact_id
715
+ # simulator_name is guaranteed set by parse_simulator_artifact (or we exit)
716
+ assert simulator_name is not None, "simulator_name must be set"
717
+
718
+ base_url = _get_base_url()
719
+ async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
720
+ try:
721
+ sim = await get_simulator_by_name.asyncio(
722
+ client=client,
723
+ name=simulator_name,
724
+ x_api_key=api_key,
725
+ )
726
+ config = sim.config or {}
727
+
728
+ # If no artifact provided, try to get data_artifact_id from server
729
+ if not artifact_id:
730
+ artifact_id = config.get("data_artifact_id")
731
+ if artifact_id:
732
+ console.print(f"[cyan]Using data_artifact_id from server:[/cyan] {artifact_id}")
733
+ else:
734
+ console.print("[yellow]No artifact specified and no data_artifact_id on server[/yellow]")
735
+
736
+ # Find most recent data review
737
+ reviews = config.get("reviews") or []
738
+ data_reviews = [r for r in reviews if r.get("review_type") == "data"]
739
+ if data_reviews:
740
+ data_reviews.sort(key=lambda r: r.get("timestamp_iso", ""), reverse=True)
741
+ return data_reviews[0]
742
+ except Exception as e:
743
+ console.print(f"[yellow]⚠️ Could not fetch simulator info: {e}[/yellow]")
744
+ return None
513
745
 
514
- recent_review = handle_async(_fetch_recent_review())
746
+ recent_review = handle_async(_fetch_artifact_info())
747
+
748
+ if artifact_id:
749
+ console.print(f"[cyan]Artifact:[/cyan] {artifact_id}")
515
750
 
516
751
  # Find Chrome extension source
517
752
  package_dir = Path(__file__).resolve().parent.parent # v1/
@@ -554,6 +789,8 @@ def review_data(
554
789
 
555
790
  console.print("[cyan]Launching Chrome with EnvGen Recorder extension...[/cyan]")
556
791
 
792
+ from playwright.async_api import async_playwright
793
+
557
794
  playwright = await async_playwright().start()
558
795
 
559
796
  browser = await playwright.chromium.launch_persistent_context(
@@ -690,12 +927,24 @@ def submit_base():
690
927
  """
691
928
  Submit base/environment artifact for review after snapshot.
692
929
 
693
- Worker submits base artifact for review after creating a snapshot.
694
- Requires status: env_in_progress
695
- Transitions to: env_review_requested
930
+ Reads simulator name and artifact ID from .sandbox.yaml (created by
931
+ plato sandbox start). Run this from the simulator directory after
932
+ creating a snapshot.
933
+
934
+ Transitions simulator from env_in_progress → env_review_requested.
935
+
936
+ USAGE:
937
+
938
+ plato pm submit base # No args needed - reads from .sandbox.yaml
696
939
 
697
- Example:
698
- plato pm submit base
940
+ PREREQUISITES:
941
+
942
+ 1. plato sandbox start --from-config
943
+ 2. plato sandbox start-services
944
+ 3. plato sandbox snapshot
945
+ 4. plato pm submit base ← you are here
946
+
947
+ Requires simulator status: env_in_progress
699
948
  """
700
949
  api_key = require_api_key()
701
950
 
@@ -706,10 +955,43 @@ def submit_base():
706
955
  )
707
956
  plato_config_path = require_sandbox_field(sandbox_data, "plato_config_path")
708
957
 
709
- # Read plato-config.yml to get simulator name
958
+ # Read plato-config.yml to get simulator name and metadata
710
959
  plato_config = read_plato_config(plato_config_path)
711
960
  simulator_name = require_plato_config_field(plato_config, "service")
712
961
 
962
+ # Extract metadata from plato-config.yml
963
+ datasets = plato_config.get("datasets", {})
964
+ base_dataset = datasets.get("base", {})
965
+ metadata = base_dataset.get("metadata", {})
966
+
967
+ # Get metadata fields
968
+ config_description = metadata.get("description")
969
+ config_license = metadata.get("license")
970
+ config_source_code_url = metadata.get("source_code_url")
971
+ config_start_url = metadata.get("start_url")
972
+ config_favicon_url = metadata.get("favicon_url") # Explicit favicon URL
973
+
974
+ # Get authentication from variables
975
+ variables = metadata.get("variables", [])
976
+ username = None
977
+ password = None
978
+ for var in variables:
979
+ if isinstance(var, dict):
980
+ var_name = var.get("name", "").lower()
981
+ var_value = var.get("value")
982
+ if var_name in ("username", "user", "email", "admin_email", "adminmail"):
983
+ username = var_value
984
+ elif var_name in ("password", "pass", "admin_password", "adminpass"):
985
+ password = var_value
986
+
987
+ # Use explicit favicon_url from config, or warn if missing
988
+ favicon_url = config_favicon_url
989
+ if not favicon_url:
990
+ console.print("[yellow]⚠️ No favicon_url in plato-config.yml metadata - favicon will not be set[/yellow]")
991
+ console.print(
992
+ "[yellow] Add 'favicon_url: https://www.google.com/s2/favicons?domain=APPNAME.com&sz=32' to metadata[/yellow]"
993
+ )
994
+
713
995
  async def _submit_base():
714
996
  base_url = _get_base_url()
715
997
 
@@ -733,6 +1015,52 @@ def submit_base():
733
1015
  console.print(f"[cyan]Current Status:[/cyan] {current_status}")
734
1016
  console.print()
735
1017
 
1018
+ # Sync metadata from plato-config.yml to server
1019
+ console.print("[cyan]Syncing metadata to server...[/cyan]")
1020
+
1021
+ # Build update request with metadata from plato-config.yml
1022
+ update_fields: dict = {}
1023
+
1024
+ if config_description:
1025
+ update_fields["description"] = config_description
1026
+ console.print(f" [dim]description:[/dim] {config_description[:50]}...")
1027
+
1028
+ if favicon_url:
1029
+ update_fields["img_url"] = favicon_url
1030
+ console.print(f" [dim]img_url:[/dim] {favicon_url}")
1031
+
1032
+ if config_license:
1033
+ update_fields["license"] = config_license
1034
+ console.print(f" [dim]license:[/dim] {config_license}")
1035
+
1036
+ if config_source_code_url:
1037
+ update_fields["source_code_url"] = config_source_code_url
1038
+ console.print(f" [dim]source_code_url:[/dim] {config_source_code_url}")
1039
+
1040
+ if config_start_url:
1041
+ update_fields["start_url"] = config_start_url
1042
+ console.print(f" [dim]start_url:[/dim] {config_start_url}")
1043
+
1044
+ if username and password:
1045
+ update_fields["authentication"] = Authentication(user=username, password=password)
1046
+ console.print(f" [dim]authentication:[/dim] {username} / {'*' * len(password)}")
1047
+
1048
+ # Always include base_artifact_id
1049
+ update_fields["base_artifact_id"] = artifact_id
1050
+
1051
+ try:
1052
+ await update_simulator.asyncio(
1053
+ client=client,
1054
+ simulator_id=simulator_id,
1055
+ body=AppApiV1SimulatorRoutesUpdateSimulatorRequest(**update_fields),
1056
+ x_api_key=api_key,
1057
+ )
1058
+ console.print("[green]✅ Metadata synced to server[/green]")
1059
+ except Exception as e:
1060
+ console.print(f"[yellow]⚠️ Could not sync metadata: {e}[/yellow]")
1061
+
1062
+ console.print()
1063
+
736
1064
  # Update simulator status
737
1065
  await update_simulator_status.asyncio(
738
1066
  client=client,
@@ -741,21 +1069,6 @@ def submit_base():
741
1069
  x_api_key=api_key,
742
1070
  )
743
1071
 
744
- # Set base_artifact_id via tag update
745
- try:
746
- await update_tag.asyncio(
747
- client=client,
748
- body=UpdateTagRequest(
749
- simulator_name=simulator_name,
750
- artifact_id=artifact_id,
751
- tag_name="base-pending-review",
752
- dataset="base",
753
- ),
754
- x_api_key=api_key,
755
- )
756
- except Exception as e:
757
- console.print(f"[yellow]⚠️ Could not set artifact tag: {e}[/yellow]")
758
-
759
1072
  console.print("[green]✅ Environment review requested successfully![/green]")
760
1073
  console.print(f"[cyan]Status:[/cyan] {current_status} → env_review_requested")
761
1074
  console.print(f"[cyan]Base Artifact:[/cyan] {artifact_id}")
@@ -765,39 +1078,48 @@ def submit_base():
765
1078
 
766
1079
  @submit_app.command(name="data")
767
1080
  def submit_data(
768
- simulator: str = typer.Option(None, "--simulator", "-s", help="Simulator name"),
769
- artifact: str = typer.Option(None, "--artifact", "-a", help="Artifact ID (required)"),
1081
+ simulator: str = typer.Option(
1082
+ None,
1083
+ "--simulator",
1084
+ "-s",
1085
+ help="Simulator name. Supports colon notation: -s sim:<artifact-uuid>",
1086
+ ),
1087
+ artifact: str = typer.Option(
1088
+ None,
1089
+ "--artifact",
1090
+ "-a",
1091
+ help="Artifact UUID to submit for data review (required).",
1092
+ ),
770
1093
  ):
771
1094
  """
772
1095
  Submit data artifact for review after data generation.
773
1096
 
774
- Worker manually specifies artifact ID for data review.
775
- Requires status: data_in_progress
776
- Transitions to: data_review_requested
1097
+ Transitions simulator from data_in_progress data_review_requested.
1098
+
1099
+ SPECIFYING SIMULATOR AND ARTIFACT (both required):
1100
+
1101
+ -s <simulator> -a <artifact-uuid> Explicit artifact
1102
+ -s <simulator>:<artifact-uuid> Colon notation (same as above)
1103
+
1104
+ EXAMPLES:
1105
+
1106
+ plato pm submit data -s espocrm -a e9c25ca5-1234-5678-9abc-def012345678
1107
+ plato pm submit data -s espocrm:e9c25ca5-1234-5678-9abc-def012345678
777
1108
 
778
- Example:
779
- plato pm submit data --simulator espocrm --artifact abc123
780
- plato pm submit data -s espocrm -a abc123
1109
+ Requires simulator status: data_in_progress
781
1110
  """
782
1111
  api_key = require_api_key()
783
1112
 
784
- # Use arg or prompt for simulator name
785
- simulator_name = simulator
786
- if not simulator_name:
787
- simulator_name = typer.prompt("Enter simulator name").strip()
788
- if not simulator_name:
789
- console.print("[red]❌ Simulator name is required[/red]")
790
- raise typer.Exit(1)
791
-
792
- # Artifact ID is required
793
- artifact_id = artifact
794
- if not artifact_id:
795
- artifact_id = typer.prompt("Enter artifact ID").strip()
796
- if not artifact_id:
797
- console.print("[red]❌ Artifact ID is required[/red]")
798
- raise typer.Exit(1)
1113
+ # Parse simulator and artifact from args (artifact IS required for data submit)
1114
+ simulator_name, artifact_id = parse_simulator_artifact(
1115
+ simulator, artifact, require_artifact=True, command_name="submit data"
1116
+ )
799
1117
 
800
1118
  async def _submit_data():
1119
+ # simulator_name and artifact_id are guaranteed set by parse_simulator_artifact with require_artifact=True
1120
+ assert simulator_name is not None, "simulator_name must be set"
1121
+ assert artifact_id is not None, "artifact_id must be set"
1122
+
801
1123
  base_url = _get_base_url()
802
1124
 
803
1125
  async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
@@ -828,7 +1150,7 @@ def submit_data(
828
1150
  x_api_key=api_key,
829
1151
  )
830
1152
 
831
- # Set data_artifact_id via tag update
1153
+ # Set data_artifact_id via tag update (simulator_name and artifact_id already asserted above)
832
1154
  try:
833
1155
  await update_tag.asyncio(
834
1156
  client=client,