plato-sdk-v2 2.0.64__py3-none-any.whl → 2.3.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 (46) hide show
  1. plato/__init__.py +0 -9
  2. plato/_sims_generator/__init__.py +19 -4
  3. plato/_sims_generator/instruction.py +203 -0
  4. plato/_sims_generator/templates/instruction/helpers.py.jinja +161 -0
  5. plato/_sims_generator/templates/instruction/init.py.jinja +43 -0
  6. plato/agents/__init__.py +99 -430
  7. plato/agents/base.py +145 -0
  8. plato/agents/build.py +61 -0
  9. plato/agents/config.py +160 -0
  10. plato/agents/logging.py +515 -0
  11. plato/agents/runner.py +191 -0
  12. plato/agents/trajectory.py +266 -0
  13. plato/chronos/models/__init__.py +1 -1
  14. plato/sims/cli.py +299 -123
  15. plato/sims/registry.py +77 -4
  16. plato/v1/cli/agent.py +88 -84
  17. plato/v1/cli/pm.py +84 -44
  18. plato/v1/cli/sandbox.py +241 -61
  19. plato/v1/cli/ssh.py +16 -4
  20. plato/v1/cli/verify.py +685 -0
  21. plato/v1/cli/world.py +3 -0
  22. plato/v1/flow_executor.py +21 -17
  23. plato/v1/models/env.py +11 -11
  24. plato/v1/sdk.py +2 -2
  25. plato/v1/sync_env.py +11 -11
  26. plato/v1/sync_flow_executor.py +21 -17
  27. plato/v1/sync_sdk.py +4 -2
  28. plato/v2/__init__.py +2 -0
  29. plato/v2/async_/environment.py +31 -0
  30. plato/v2/async_/session.py +72 -4
  31. plato/v2/sync/environment.py +31 -0
  32. plato/v2/sync/session.py +72 -4
  33. plato/worlds/README.md +71 -56
  34. plato/worlds/__init__.py +56 -18
  35. plato/worlds/base.py +578 -93
  36. plato/worlds/config.py +276 -74
  37. plato/worlds/runner.py +475 -80
  38. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/METADATA +3 -3
  39. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/RECORD +41 -36
  40. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/entry_points.txt +1 -0
  41. plato/agents/callback.py +0 -246
  42. plato/world/__init__.py +0 -44
  43. plato/world/base.py +0 -267
  44. plato/world/config.py +0 -139
  45. plato/world/types.py +0 -47
  46. {plato_sdk_v2-2.0.64.dist-info → plato_sdk_v2-2.3.4.dist-info}/WHEEL +0 -0
plato/v1/cli/agent.py CHANGED
@@ -6,13 +6,16 @@ import re
6
6
  import shutil
7
7
  import subprocess
8
8
  import sys
9
- import tempfile
10
9
  from pathlib import Path
10
+ from typing import TYPE_CHECKING
11
11
 
12
12
  import typer
13
13
 
14
14
  from plato.v1.cli.utils import console, require_api_key
15
15
 
16
+ if TYPE_CHECKING:
17
+ from plato.agents.build import BuildConfig
18
+
16
19
 
17
20
  def _extract_schemas(pkg_path: Path, package_name: str) -> tuple[dict | None, dict | None, dict | None]:
18
21
  """Extract Config, BuildConfig, and SecretsConfig schemas from the agent package.
@@ -279,8 +282,7 @@ def _publish_agent_image(
279
282
  description: str,
280
283
  dry_run: bool,
281
284
  schema_data: dict | None = None,
282
- template_variables: dict[str, str] | None = None,
283
- secrets_schema: dict | None = None,
285
+ build_config: "BuildConfig | None" = None,
284
286
  ) -> None:
285
287
  """Common logic for publishing an agent Docker image to ECR."""
286
288
  import httpx
@@ -298,9 +300,27 @@ def _publish_agent_image(
298
300
  console.print("[cyan]Building Docker image...[/cyan]")
299
301
  local_tag = f"{agent_name}:{version}"
300
302
 
303
+ # Build docker command with build args from config
304
+ docker_cmd = ["docker", "build", "--progress=plain", "-t", local_tag]
305
+
306
+ # Add --target prod if Dockerfile has multi-stage builds
307
+ dockerfile_path = build_path / "Dockerfile"
308
+ if dockerfile_path.exists():
309
+ dockerfile_content = dockerfile_path.read_text()
310
+ if "FROM" in dockerfile_content and "AS prod" in dockerfile_content:
311
+ docker_cmd.extend(["--target", "prod"])
312
+ console.print("[cyan]Using target: prod[/cyan]")
313
+
314
+ # Add build args from build config's env dict
315
+ if build_config and build_config.env:
316
+ for key, value in build_config.env.items():
317
+ docker_cmd.extend(["--build-arg", f"{key}={value}"])
318
+
319
+ docker_cmd.append(str(build_path))
320
+
301
321
  # Use Popen to stream output in real-time
302
322
  process = subprocess.Popen(
303
- ["docker", "build", "--progress=plain", "-t", local_tag, str(build_path)],
323
+ docker_cmd,
304
324
  stdout=subprocess.PIPE,
305
325
  stderr=subprocess.STDOUT,
306
326
  text=True,
@@ -309,6 +329,9 @@ def _publish_agent_image(
309
329
 
310
330
  # Stream build output
311
331
  build_output = []
332
+ if process.stdout is None:
333
+ console.print("[red]Error: Failed to capture Docker build output[/red]")
334
+ raise typer.Exit(1)
312
335
  for line in iter(process.stdout.readline, ""):
313
336
  line = line.rstrip()
314
337
  if line:
@@ -405,10 +428,6 @@ def _publish_agent_image(
405
428
  "description": description,
406
429
  "config_schema": schema_data,
407
430
  }
408
- if template_variables:
409
- registration_data["template_variables"] = template_variables
410
- if secrets_schema:
411
- registration_data["secrets_schema"] = secrets_schema
412
431
  response = client.post(
413
432
  "/v2/agents/register",
414
433
  json=registration_data,
@@ -431,6 +450,11 @@ def _publish_agent_image(
431
450
  console.print(f"[cyan]Artifact ID:[/cyan] {reg_result['artifact_id']}")
432
451
  console.print(f"[cyan]Image:[/cyan] {ecr_image}")
433
452
 
453
+ # Clean up local images after successful push
454
+ console.print("[dim]Cleaning up local images...[/dim]")
455
+ subprocess.run(["docker", "rmi", local_tag], capture_output=True)
456
+ subprocess.run(["docker", "rmi", ecr_image], capture_output=True)
457
+
434
458
 
435
459
  def _publish_package(path: str, repo: str, dry_run: bool = False):
436
460
  """
@@ -545,6 +569,8 @@ def _publish_package(path: str, repo: str, dry_run: bool = False):
545
569
  upload_url = f"{api_url}/v2/pypi/{repo}/"
546
570
  console.print(f"\n[cyan]Uploading to {upload_url}...[/cyan]")
547
571
 
572
+ # api_key is guaranteed to be set (checked earlier when not dry_run)
573
+ assert api_key is not None, "api_key must be set when not in dry_run mode"
548
574
  try:
549
575
  result = subprocess.run(
550
576
  [
@@ -560,6 +586,7 @@ def _publish_package(path: str, repo: str, dry_run: bool = False):
560
586
  ],
561
587
  capture_output=True,
562
588
  text=True,
589
+ check=False,
563
590
  )
564
591
 
565
592
  if result.returncode == 0:
@@ -797,39 +824,10 @@ def agent_push(
797
824
  console.print(f"[yellow]Failed:[/yellow] {', '.join(failed)}")
798
825
  return
799
826
 
800
- # Check if target is a Harbor agent name
801
- if target in HARBOR_AGENTS:
802
- harbor_version = _get_harbor_version()
803
- console.print(f"[cyan]Harbor agent:[/cyan] {target}")
804
- console.print(f"[cyan]Harbor version:[/cyan] {harbor_version}")
805
-
806
- install_script = _get_harbor_install_script(target)
807
- if not install_script:
808
- console.print(f"[red]Error: Could not get install script for '{target}'[/red]")
809
- console.print("[yellow]Make sure harbor is installed: pip install 'plato-sdk-v2[agents]'[/yellow]")
810
- raise typer.Exit(1)
811
-
812
- with tempfile.TemporaryDirectory() as tmpdir:
813
- tmppath = Path(tmpdir)
814
- aux_files = HARBOR_AGENT_AUX_FILES.get(target, [])
815
- (tmppath / "Dockerfile").write_text(_build_harbor_dockerfile(aux_files))
816
- (tmppath / "install.sh").write_text(install_script)
817
- _copy_harbor_aux_files(target, tmppath)
818
-
819
- _publish_agent_image(
820
- agent_name=target,
821
- version=harbor_version,
822
- build_path=tmppath,
823
- description=f"Harbor agent: {target}",
824
- dry_run=dry_run,
825
- )
826
- return
827
-
828
- # Otherwise, treat as a path to custom agent
827
+ # Treat target as a path to agent directory
829
828
  pkg_path = Path(target).resolve()
830
829
  if not pkg_path.exists():
831
- console.print(f"[red]Error: '{target}' is not a Harbor agent name or valid path[/red]")
832
- console.print(f"\n[yellow]Harbor agents:[/yellow] {', '.join(HARBOR_AGENTS.keys())}")
830
+ console.print(f"[red]Error: '{target}' is not a valid path[/red]")
833
831
  raise typer.Exit(1)
834
832
 
835
833
  _push_single_agent(pkg_path, dry_run)
@@ -837,6 +835,8 @@ def agent_push(
837
835
 
838
836
  def _push_single_agent(pkg_path: Path, dry_run: bool) -> None:
839
837
  """Push a single custom agent from a directory."""
838
+ from plato.agents.build import BuildConfig
839
+
840
840
  # Check for Dockerfile
841
841
  if not (pkg_path / "Dockerfile").exists():
842
842
  console.print(f"[red]Error: No Dockerfile found at {pkg_path}[/red]")
@@ -866,56 +866,61 @@ def _push_single_agent(pkg_path: Path, dry_run: bool) -> None:
866
866
  console.print("[red]Error: No version in pyproject.toml[/red]")
867
867
  raise typer.Exit(1)
868
868
 
869
- # Extract short name
869
+ # Extract short name (remove common prefixes)
870
870
  short_name = package_name
871
- for prefix in ("plato-agent-", "plato-"):
872
- if short_name.startswith(prefix):
873
- short_name = short_name[len(prefix) :]
871
+ for suffix in ("-agent",):
872
+ if short_name.endswith(suffix):
873
+ short_name = short_name[: -len(suffix)]
874
874
  break
875
875
 
876
- # Extract Config (runtime), BuildConfig (build-time), and SecretsConfig schemas
877
- console.print("[cyan]Looking for Config, BuildConfig, and SecretsConfig classes...[/cyan]")
878
- schema_data, build_config_schema, secrets_schema = _extract_schemas(pkg_path, package_name)
876
+ # Load build config from pyproject.toml (optional, for build args)
877
+ build_config = None
878
+ try:
879
+ build_config = BuildConfig.from_pyproject(pkg_path)
880
+ if build_config.env:
881
+ console.print(f"[cyan]Build args:[/cyan] {list(build_config.env.keys())}")
882
+ except Exception:
883
+ pass # Build config is optional
879
884
 
880
- # Fall back to schema.json file for config_schema
881
- if schema_data is None:
882
- schema_file = pkg_path / "schema.json"
883
- if schema_file.exists():
884
- console.print(f"[cyan]Loading schema from {schema_file}[/cyan]")
885
- try:
886
- with open(schema_file) as f:
887
- schema_data = json.load(f)
888
- except Exception as e:
889
- console.print(f"[yellow]Warning: Failed to load schema.json: {e}[/yellow]")
885
+ # Load schema from entry point defined in pyproject.toml
886
+ schema_data = None
887
+ entry_points_config = pyproject.get("project", {}).get("entry-points", {}).get("plato.agents", {})
890
888
 
891
- if schema_data:
892
- console.print("[green]Config schema found (runtime config)[/green]")
889
+ if not entry_points_config:
890
+ console.print("[yellow]No plato.agents entry point in pyproject.toml - agent will have no schema[/yellow]")
893
891
  else:
894
- console.print("[yellow]No Config schema found[/yellow]")
892
+ # Get the first (and typically only) entry point
893
+ # Format is: agent_name = "module_name:ClassName"
894
+ for ep_name, ep_value in entry_points_config.items():
895
+ try:
896
+ if ":" not in ep_value:
897
+ console.print(f"[yellow]Invalid entry point format '{ep_value}' - expected 'module:Class'[/yellow]")
898
+ continue
895
899
 
896
- if secrets_schema:
897
- console.print("[green]SecretsConfig schema found[/green]")
898
- else:
899
- console.print("[yellow]No SecretsConfig schema found[/yellow]")
900
-
901
- # Extract template variables from BuildConfig schema
902
- template_variables = _extract_template_variables(build_config_schema)
903
- if template_variables:
904
- console.print(f"[cyan]Template variables (from BuildConfig):[/cyan] {template_variables}")
905
- elif build_config_schema:
906
- console.print("[yellow]BuildConfig found but no template variables extracted[/yellow]")
907
-
908
- # If this is a Harbor agent wrapper, generate install.sh from Harbor template
909
- if short_name in HARBOR_AGENTS:
910
- console.print(f"[cyan]Generating install.sh from Harbor template for {short_name}...[/cyan]")
911
- install_script = _get_harbor_install_script(short_name, template_variables)
912
- if install_script:
913
- (pkg_path / "install.sh").write_text(install_script)
914
- console.print("[green]install.sh generated[/green]")
915
- # Copy auxiliary files if needed
916
- _copy_harbor_aux_files(short_name, pkg_path)
917
- else:
918
- console.print("[yellow]Warning: Could not generate install.sh from Harbor template[/yellow]")
900
+ module_name, class_name = ep_value.split(":", 1)
901
+
902
+ # Add src/ to path and import
903
+ import sys
904
+
905
+ src_path = pkg_path / "src"
906
+ if src_path.exists():
907
+ sys.path.insert(0, str(src_path))
908
+
909
+ try:
910
+ module = __import__(module_name, fromlist=[class_name])
911
+ agent_cls = getattr(module, class_name)
912
+ schema_data = agent_cls.get_schema()
913
+ console.print(f"[green]Loaded schema from {class_name}[/green]")
914
+ break
915
+ finally:
916
+ if src_path.exists() and str(src_path) in sys.path:
917
+ sys.path.remove(str(src_path))
918
+
919
+ except Exception as e:
920
+ console.print(f"[yellow]Failed to load schema from entry point: {e}[/yellow]")
921
+
922
+ if not schema_data:
923
+ console.print("[yellow]No schema found (agent will have no config validation)[/yellow]")
919
924
 
920
925
  _publish_agent_image(
921
926
  agent_name=short_name,
@@ -924,8 +929,7 @@ def _push_single_agent(pkg_path: Path, dry_run: bool) -> None:
924
929
  description=description or f"Custom agent: {short_name}",
925
930
  dry_run=dry_run,
926
931
  schema_data=schema_data,
927
- template_variables=template_variables,
928
- secrets_schema=secrets_schema,
932
+ build_config=build_config,
929
933
  )
930
934
 
931
935
 
plato/v1/cli/pm.py CHANGED
@@ -10,10 +10,6 @@ from pathlib import Path
10
10
 
11
11
  import httpx
12
12
  import typer
13
-
14
- # UUID pattern for detecting artifact IDs in sim:artifact notation
15
- 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)
16
- from playwright.async_api import async_playwright
17
13
  from rich.table import Table
18
14
 
19
15
  from plato._generated.api.v1.env import get_simulator_by_name, get_simulators
@@ -43,9 +39,17 @@ from plato.v1.cli.utils import (
43
39
  require_sandbox_field,
44
40
  require_sandbox_state,
45
41
  )
42
+ from plato.v1.cli.verify import pm_verify_app
46
43
  from plato.v2.async_.client import AsyncPlato
47
44
  from plato.v2.types import Env
48
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
+
49
53
  # =============================================================================
50
54
  # APP STRUCTURE
51
55
  # =============================================================================
@@ -58,6 +62,7 @@ submit_app = typer.Typer(help="Submit simulator artifacts for review")
58
62
  pm_app.add_typer(list_app, name="list")
59
63
  pm_app.add_typer(review_app, name="review")
60
64
  pm_app.add_typer(submit_app, name="submit")
65
+ pm_app.add_typer(pm_verify_app, name="verify")
61
66
 
62
67
 
63
68
  # =============================================================================
@@ -340,6 +345,9 @@ def review_base(
340
345
  try:
341
346
  http_client = plato._http
342
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
+
343
351
  # Get simulator by name
344
352
  sim = await get_simulator_by_name.asyncio(
345
353
  client=http_client,
@@ -353,7 +361,7 @@ def review_base(
353
361
  console.print(f"[cyan]Current status:[/cyan] {current_status}")
354
362
 
355
363
  # Use provided artifact ID or fall back to base_artifact_id from server config
356
- artifact_id = artifact_id_input if artifact_id_input else current_config.get("base_artifact_id")
364
+ artifact_id: str | None = artifact_id_input if artifact_id_input else current_config.get("base_artifact_id")
357
365
  if not artifact_id:
358
366
  console.print("[red]❌ No artifact ID provided.[/red]")
359
367
  console.print(
@@ -390,6 +398,8 @@ def review_base(
390
398
 
391
399
  # Launch Playwright browser and login
392
400
  console.print("[cyan]Launching browser and logging in...[/cyan]")
401
+ from playwright.async_api import async_playwright
402
+
393
403
  playwright = await async_playwright().start()
394
404
  browser = await playwright.chromium.launch(headless=False)
395
405
 
@@ -403,44 +413,57 @@ def review_base(
403
413
  if public_url:
404
414
  await page.goto(public_url)
405
415
 
406
- # If skip_review, check state and exit without interactive loop
407
- if skip_review:
408
- console.print("\n[cyan]Checking environment state after login...[/cyan]")
409
- try:
410
- state_response = await sessions_state.asyncio(
411
- client=http_client,
412
- session_id=session.session_id,
413
- merge_mutations=True,
414
- x_api_key=api_key,
415
- )
416
- if state_response and state_response.results:
417
- has_mutations = False
418
- for jid, result in state_response.results.items():
419
- state_data = result.state if hasattr(result, "state") else result
420
- if isinstance(state_data, dict):
421
- mutations = state_data.pop("mutations", [])
422
- console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
423
- console.print("\n[bold]State:[/bold]")
424
- console.print(json.dumps(state_data, indent=2, default=str))
425
- if mutations:
426
- has_mutations = True
427
- console.print(f"\n[bold red]Mutations ({len(mutations)}):[/bold red]")
428
- console.print(json.dumps(mutations, indent=2, default=str))
429
- else:
430
- console.print("\n[green]No mutations recorded[/green]")
431
-
432
- if has_mutations:
433
- console.print("\n[bold red]⚠️ WARNING: Login flow created mutations![/bold red]")
434
- console.print("[yellow]The login flow should NOT modify database state.[/yellow]")
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]")
435
449
  else:
436
- console.print(
437
- "\n[bold green]✅ Login flow verified - no mutations created[/bold green]"
438
- )
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]")
439
458
  else:
440
- console.print("[yellow]No state data available[/yellow]")
441
- except Exception as e:
442
- console.print(f"[red] Error getting state: {e}[/red]")
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]")
443
464
 
465
+ # If skip_review, exit without interactive loop
466
+ if skip_review:
444
467
  console.print("\n[cyan]Skipping interactive review (--skip-review)[/cyan]")
445
468
  return
446
469
 
@@ -492,9 +515,16 @@ def review_base(
492
515
  if state_response and state_response.results:
493
516
  for jid, result in state_response.results.items():
494
517
  state_data = result.state if hasattr(result, "state") else result
518
+ console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
519
+
495
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
+
496
527
  mutations = state_data.pop("mutations", [])
497
- console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
498
528
  console.print("\n[bold]State:[/bold]")
499
529
  console.print(json.dumps(state_data, indent=2, default=str))
500
530
  if mutations:
@@ -503,7 +533,6 @@ def review_base(
503
533
  else:
504
534
  console.print("\n[yellow]No mutations recorded[/yellow]")
505
535
  else:
506
- console.print(f"\n[bold cyan]Job {jid}:[/bold cyan]")
507
536
  console.print(json.dumps(state_data, indent=2, default=str))
508
537
  else:
509
538
  console.print("[yellow]No state data available[/yellow]")
@@ -579,9 +608,11 @@ def review_base(
579
608
  console.print(f"[cyan]Status:[/cyan] {current_status} → {new_status}")
580
609
 
581
610
  # If passed, automatically tag artifact as prod-latest
582
- if outcome == "pass":
611
+ if outcome == "pass" and artifact_id:
583
612
  console.print("\n[cyan]Tagging artifact as prod-latest...[/cyan]")
584
613
  try:
614
+ # simulator_name and artifact_id are guaranteed to be set at this point
615
+ assert simulator_name is not None
585
616
  await update_tag.asyncio(
586
617
  client=http_client,
587
618
  body=UpdateTagRequest(
@@ -681,6 +712,9 @@ def review_data(
681
712
 
682
713
  async def _fetch_artifact_info():
683
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
+
684
718
  base_url = _get_base_url()
685
719
  async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
686
720
  try:
@@ -755,6 +789,8 @@ def review_data(
755
789
 
756
790
  console.print("[cyan]Launching Chrome with EnvGen Recorder extension...[/cyan]")
757
791
 
792
+ from playwright.async_api import async_playwright
793
+
758
794
  playwright = await async_playwright().start()
759
795
 
760
796
  browser = await playwright.chromium.launch_persistent_context(
@@ -1080,6 +1116,10 @@ def submit_data(
1080
1116
  )
1081
1117
 
1082
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
+
1083
1123
  base_url = _get_base_url()
1084
1124
 
1085
1125
  async with httpx.AsyncClient(base_url=base_url, timeout=60.0) as client:
@@ -1110,7 +1150,7 @@ def submit_data(
1110
1150
  x_api_key=api_key,
1111
1151
  )
1112
1152
 
1113
- # Set data_artifact_id via tag update
1153
+ # Set data_artifact_id via tag update (simulator_name and artifact_id already asserted above)
1114
1154
  try:
1115
1155
  await update_tag.asyncio(
1116
1156
  client=client,